@kangsicj/nexussync 1.0.1 → 1.2.0
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 +3 -2
- package/dist/NexusSync.meta.js +3 -5
- package/dist/NexusSync.user.js +599 -77
- 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)
|
|
@@ -69,7 +69,8 @@ No subtitles on that video? Turn on live speech recognition with translation int
|
|
|
69
69
|
|
|
70
70
|
## ✨ Little things that matter
|
|
71
71
|
|
|
72
|
-
- **Next episode, together** — move on to the next episode and you stay in the room; friends follow with one tap.
|
|
72
|
+
- **Next episode, together** — move on to the next episode and you stay in the room; friends follow with one tap. Near the end, an *Up next* card counts down and opens it for you.
|
|
73
|
+
- **Cinema by itself** — open a page with one movie or episode and it switches to cinema as soon as the video is ready, keeping the site's player so you can still pick subtitles or a server. Turn it off per site.
|
|
73
74
|
- **Make it yours** — pick your name and colour; your bubbles and doodles wear it on every screen.
|
|
74
75
|
- **Calm, never jumpy** — phones going to sleep, slow Wi-Fi or a player restarting on its own never pause the whole room.
|
|
75
76
|
- **No accidental pauses** — tapping the chat, a doodle or a reaction never nudges the video for anyone.
|
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.0
|
|
11
|
+
// @version 1.2.0
|
|
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 *://*/*
|
|
@@ -32,5 +31,4 @@
|
|
|
32
31
|
// @connect api.anthropic.com
|
|
33
32
|
// @connect api-free.deepl.com
|
|
34
33
|
// @connect api.deepl.com
|
|
35
|
-
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js#sha256=95f4d1cbf099f57161b664bc048426ec3df92637801a4c79116e83315aa787e7
|
|
36
34
|
// ==/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.0
|
|
11
|
+
// @version 1.2.0
|
|
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 *://*/*
|
|
@@ -32,20 +31,23 @@
|
|
|
32
31
|
// @connect api.anthropic.com
|
|
33
32
|
// @connect api-free.deepl.com
|
|
34
33
|
// @connect api.deepl.com
|
|
35
|
-
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js#sha256=95f4d1cbf099f57161b664bc048426ec3df92637801a4c79116e83315aa787e7
|
|
36
34
|
// ==/UserScript==
|
|
37
35
|
|
|
38
36
|
(async function () {
|
|
39
37
|
'use strict';
|
|
40
38
|
|
|
39
|
+
// lz-string 1.5.0 — MIT License, © pieroxy (https://github.com/pieroxy/lz-string). Bundled so
|
|
40
|
+
// no extra file has to be fetched (and verified) by the userscript manager.
|
|
41
|
+
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; })();
|
|
42
|
+
|
|
41
43
|
// The version of the code that is actually running (the manager's metadata can lag
|
|
42
44
|
// behind when an old copy is still installed).
|
|
43
|
-
const NXU_VERSION = '1.0
|
|
45
|
+
const NXU_VERSION = '1.2.0';
|
|
44
46
|
// Rank used by the one-instance guard below and to tell newer builds apart in a room.
|
|
45
47
|
// It is independent of the public version (1.0.0 is the first public release, after
|
|
46
48
|
// private builds that reached ranks up to 10.2.0), so every older copy still installed
|
|
47
49
|
// steps aside instead of this release.
|
|
48
|
-
const NXU_RANK = '11.0
|
|
50
|
+
const NXU_RANK = '11.2.0';
|
|
49
51
|
const NXU_SESSION = Math.random().toString(36).slice(2, 10); // this page load // raise with every release
|
|
50
52
|
// App icon (same artwork as assets/icon.svg / @icon), inlined so it shows offline too.
|
|
51
53
|
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 +173,7 @@
|
|
|
171
173
|
'nxu_idle_timeout', 'nxu_sub_size', 'nxu_sub_opacity', 'nxu_sub_position', 'nxu_speaker_language',
|
|
172
174
|
'nxu_subtitle_language', 'nxu_audio_mode', 'nxu_translation_provider', 'nxu_stt_local_first',
|
|
173
175
|
'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_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
|
|
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
|
|
175
177
|
]);
|
|
176
178
|
|
|
177
179
|
// ───────────────────────── Cross-origin HTTP (GM xhr first, fetch fallback)
|
|
@@ -240,7 +242,7 @@
|
|
|
240
242
|
hardSyncTolerance: 2.5,
|
|
241
243
|
softSyncTolerance: 0.25,
|
|
242
244
|
syncHeartbeatMs: 3000,
|
|
243
|
-
emojis: ['
|
|
245
|
+
emojis: ['😂', '😭', '😡', '💦', '🍪', '☕', '😘', '🖕'],
|
|
244
246
|
chunkSize: 2000,
|
|
245
247
|
relayBatchInterval: 250,
|
|
246
248
|
relayBatchIntervalTight: 80,
|
|
@@ -382,7 +384,8 @@
|
|
|
382
384
|
videoHookController: null,
|
|
383
385
|
hostHeartbeatTimer: null,
|
|
384
386
|
_presenceTimer: null,
|
|
385
|
-
|
|
387
|
+
// Rises across page loads too (a reloaded page keeps its identity when it rejoins).
|
|
388
|
+
_videoSequence: Date.now() - 1700000000000,
|
|
386
389
|
_lastVideoSeqBySource: new Map(),
|
|
387
390
|
_processedVideoEvents: new Map(),
|
|
388
391
|
_acceptedVideoState: null,
|
|
@@ -621,8 +624,8 @@
|
|
|
621
624
|
travelBack: 'Back in the room', travelMoved: 'Moved to “{title}” · tap to go together', travelGoing: 'Going together…',
|
|
622
625
|
chatTrOn: 'Auto-translate: on', chatTrOff: 'Auto-translate: off', chatTrShowOrig: 'Tap to see the original', chatTrShowTr: 'Tap to see the translation', chatTrFrom: 'Translated from {lang}',
|
|
623
626
|
inkClosing: 'Closing soon — move or tap to keep drawing',
|
|
624
|
-
talkMenu: 'Chat', inkMenu: 'Quick sketch', quickEdit: 'Edit quick replies', quickEditHint: 'Separate with |', quickReset: 'Back to the default replies', save: 'Save',
|
|
625
|
-
talkTitle: 'Chat bubble', talkPlaceholder: 'Say something…', talkReplyTo: '↩ Reply to {name}', talkClose: 'Close', talkSend: 'Send', talkFull: 'Open the
|
|
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',
|
|
628
|
+
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',
|
|
626
629
|
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',
|
|
627
630
|
you: 'You', roomBuffering: '⏳ {name} is loading… everyone waits', roomResume: '▶️ Everyone is ready — playing together', roomRate: '⏩ Speed {rate}x synced',
|
|
628
631
|
youBuffering: '⏳ Your video is loading — the room waits for you', shieldResumeTitle: 'Tap to rejoin playback', shieldResumeText: 'The browser paused this video. Tap once to continue in sync with the room.',
|
|
@@ -704,8 +707,8 @@
|
|
|
704
707
|
travelBack: '방에 다시 연결했어요', travelMoved: '「{title}」(으)로 이동했어요 · 눌러서 같이 가기', travelGoing: '같이 이동하는 중…',
|
|
705
708
|
chatTrOn: '자동 번역 켜짐', chatTrOff: '자동 번역 꺼짐', chatTrShowOrig: '눌러서 원문 보기', chatTrShowTr: '눌러서 번역 보기', chatTrFrom: '{lang}에서 번역됨',
|
|
706
709
|
inkClosing: '곧 닫혀요 — 계속 그리려면 움직이거나 눌러 주세요',
|
|
707
|
-
talkMenu: '말풍선 채팅', inkMenu: '잠깐 그리기', quickEdit: '빠른 답장 편집', quickEditHint: '| 로 구분', quickReset: '기본 답장으로', save: '저장',
|
|
708
|
-
talkTitle: '말풍선 채팅', talkPlaceholder: '메시지 보내기…', talkReplyTo: '↩ {name} 님에게 답장', talkClose: '닫기', talkSend: '보내기', talkFull: '전체
|
|
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: '저장',
|
|
711
|
+
talkTitle: '말풍선 채팅', talkPlaceholder: '메시지 보내기…', talkReplyTo: '↩ {name} 님에게 답장', talkClose: '닫기', talkSend: '보내기', talkFull: '지금까지의 대화 기록 전체 보기', talkHistory: '대화 기록',
|
|
709
712
|
inkTitle: '잠깐 그리기 (몇 초 뒤 사라짐)', inkHint: '화면에 그려 보세요 — 몇 초 뒤 사라져요', inkDone: '완료', inkBubble: '님이 화면에 그리고 있어요 — 탭해서 같이 그리기',
|
|
710
713
|
you: '나', roomBuffering: '⏳ {name} 기기 로딩 중… 모두 잠시 대기', roomResume: '▶️ 모두 준비됨 — 함께 재생합니다', roomRate: '⏩ {rate}배속 동기화',
|
|
711
714
|
youBuffering: '⏳ 이 기기 영상 로딩 중 — 방이 기다려 줍니다', shieldResumeTitle: '탭해서 다시 재생', shieldResumeText: '브라우저가 이 영상을 멈췄습니다. 한 번 탭하면 방과 맞춰 이어서 재생합니다.',
|
|
@@ -1198,7 +1201,7 @@
|
|
|
1198
1201
|
return v === 'cover' || v === 'fill' ? v : 'contain';
|
|
1199
1202
|
},
|
|
1200
1203
|
// Cinema controls mode: the site's own player UI is hidden so only one set shows.
|
|
1201
|
-
nexusControls() { return IS_TOP ?
|
|
1204
|
+
nexusControls() { return IS_TOP ? cinemaControlsMode() !== 'site' : this._frameNexus === true; },
|
|
1202
1205
|
_startAmbient(video, root) {
|
|
1203
1206
|
this._stopAmbient();
|
|
1204
1207
|
if (!video || !root || !this.ambientEnabled()) return;
|
|
@@ -1379,8 +1382,12 @@
|
|
|
1379
1382
|
else this._frameFit = fit;
|
|
1380
1383
|
this._restyle();
|
|
1381
1384
|
},
|
|
1382
|
-
setControls(mode) {
|
|
1383
|
-
|
|
1385
|
+
setControls(mode, { save = true } = {}) {
|
|
1386
|
+
// save:false (automatic cinema) uses the site's player for this visit only.
|
|
1387
|
+
if (IS_TOP) {
|
|
1388
|
+
if (save) { STATE.controlsOverride = null; STORE.set('nxu_cinema_controls', mode === 'site' ? 'site' : 'nexus'); }
|
|
1389
|
+
else STATE.controlsOverride = mode === 'site' ? 'site' : 'nexus';
|
|
1390
|
+
}
|
|
1384
1391
|
this._restyle();
|
|
1385
1392
|
},
|
|
1386
1393
|
|
|
@@ -1489,6 +1496,8 @@
|
|
|
1489
1496
|
try { (document.exitFullscreen || document.webkitExitFullscreen)?.call(document)?.catch?.(() => { }); } catch (_) { }
|
|
1490
1497
|
}
|
|
1491
1498
|
if (!on) this._ownFullscreen = false;
|
|
1499
|
+
if (!on) { STATE.controlsOverride = null; AUTO_CINEMA.onExit(); }
|
|
1500
|
+
if (on) AUTO_CINEMA.onEnter();
|
|
1492
1501
|
if (!on && ROTATE.deg) ROTATE._tick(); // leaving cinema also leaves landscape view at once
|
|
1493
1502
|
CINEMA_UI.sync();
|
|
1494
1503
|
updateMediaButtons();
|
|
@@ -1836,7 +1845,9 @@
|
|
|
1836
1845
|
},
|
|
1837
1846
|
_decide(side) {
|
|
1838
1847
|
if (side) this._lastSide = side;
|
|
1839
|
-
|
|
1848
|
+
// Turned on with ↻: stays in landscape, but follows the phone to the other side
|
|
1849
|
+
// (left or right edge down) instead of staying stuck on the first one.
|
|
1850
|
+
if (this.manual) { if (side && this.deg && side !== this.deg) this._apply(side); return; }
|
|
1840
1851
|
if (!side) {
|
|
1841
1852
|
this.suppress = false;
|
|
1842
1853
|
if (this.deg) this._leave();
|
|
@@ -3638,7 +3649,9 @@
|
|
|
3638
3649
|
} else if (msg.t === 'pong') {
|
|
3639
3650
|
const t3 = Date.now();
|
|
3640
3651
|
const rtt = t3 - msg.t1;
|
|
3641
|
-
|
|
3652
|
+
// Everyone shares the server clock; the direct link only stands in until
|
|
3653
|
+
// this device has its own server measurement.
|
|
3654
|
+
if (!STATE._serverClockAt) STATE.clockOffset = msg.t2 - (msg.t1 + rtt / 2);
|
|
3642
3655
|
STATE.rttMs = rtt;
|
|
3643
3656
|
updateRttHUD(rtt);
|
|
3644
3657
|
logDebug(`P2P Clock Offset: ${STATE.clockOffset.toFixed(2)}ms`, "success");
|
|
@@ -3828,6 +3841,11 @@
|
|
|
3828
3841
|
|
|
3829
3842
|
function _writeVideoStateFields(state) {
|
|
3830
3843
|
yVideo.set('state', _stateForDoc(state));
|
|
3844
|
+
// Each device also keeps its latest decision under its own key. Two devices writing the
|
|
3845
|
+
// shared 'state' at the same moment are settled by Yjs in a fixed order — on a slow link
|
|
3846
|
+
// the same device always won, so the other one's pause could never get through. Keys
|
|
3847
|
+
// that only one device writes are never lost; readers pick the newest decision.
|
|
3848
|
+
yVideo.set(DEVICE_KEY_PREFIX + STATE.userId, _stateForDoc(state));
|
|
3831
3849
|
// Legacy mirror for clients that have not upgraded yet.
|
|
3832
3850
|
yVideo.set('eventId', state.eventId);
|
|
3833
3851
|
yVideo.set('sourceDeviceId', state.sourceDeviceId);
|
|
@@ -3874,8 +3892,12 @@
|
|
|
3874
3892
|
function _repairAcceptedVideoState() {
|
|
3875
3893
|
const accepted = STATE._acceptedVideoState;
|
|
3876
3894
|
if (!accepted || !ydoc || !yVideo) return;
|
|
3895
|
+
// Our own key carries what this device holds as the decision; once it shows the
|
|
3896
|
+
// accepted one, every reader can see it and there is nothing to rewrite.
|
|
3897
|
+
const own = _normalizeVideoState(yVideo.get(DEVICE_KEY_PREFIX + STATE.userId));
|
|
3898
|
+
if (own && own.eventId === accepted.eventId) return;
|
|
3877
3899
|
const current = _normalizeVideoState(yVideo.get('state'));
|
|
3878
|
-
if (current && current.eventId === accepted.eventId) return;
|
|
3900
|
+
if (current && current.eventId === accepted.eventId && own) return;
|
|
3879
3901
|
// Writing back is only for a decision a stale writer covered up, and at most every
|
|
3880
3902
|
// 3 s: two devices that disagree must not keep rewriting the doc at each other.
|
|
3881
3903
|
if (accepted.action === 'heartbeat') return;
|
|
@@ -3886,13 +3908,29 @@
|
|
|
3886
3908
|
logDebug(`[Authority] Restored accepted media state ${accepted.eventId}`, 'warn');
|
|
3887
3909
|
}
|
|
3888
3910
|
|
|
3911
|
+
const DEVICE_KEY_PREFIX = 's:';
|
|
3912
|
+
// The newest decision among the shared key and every device's own key.
|
|
3913
|
+
function _newestVideoState(keys) {
|
|
3914
|
+
let best = null;
|
|
3915
|
+
for (const key of keys) {
|
|
3916
|
+
if (key !== 'state' && !(key.startsWith(DEVICE_KEY_PREFIX) && key !== DEVICE_KEY_PREFIX + STATE.userId)) continue;
|
|
3917
|
+
const st = _normalizeVideoState(yVideo.get(key));
|
|
3918
|
+
if (st && (!best || _isVideoStateNewer(st, best))) best = st;
|
|
3919
|
+
}
|
|
3920
|
+
return best;
|
|
3921
|
+
}
|
|
3889
3922
|
function _readObservedVideoState(event) {
|
|
3890
3923
|
if (!yVideo) return null;
|
|
3891
|
-
if (event && event.keysChanged
|
|
3892
|
-
|
|
3924
|
+
if (event && event.keysChanged) {
|
|
3925
|
+
const keys = [...event.keysChanged].filter(k => k === 'state' || k.startsWith(DEVICE_KEY_PREFIX));
|
|
3926
|
+
if (keys.length) return _newestVideoState(keys);
|
|
3893
3927
|
}
|
|
3894
3928
|
return _readLegacyVideoState();
|
|
3895
3929
|
}
|
|
3930
|
+
function _currentDocVideoState() {
|
|
3931
|
+
if (!yVideo) return null;
|
|
3932
|
+
return _newestVideoState([...yVideo.keys()]) || _readLegacyVideoState();
|
|
3933
|
+
}
|
|
3896
3934
|
|
|
3897
3935
|
function _acceptRemoteVideoState(rawState) {
|
|
3898
3936
|
const state = _normalizeVideoState(rawState);
|
|
@@ -3916,7 +3954,7 @@
|
|
|
3916
3954
|
|
|
3917
3955
|
function _getVideoState() {
|
|
3918
3956
|
if (!yVideo) return STATE._acceptedVideoState;
|
|
3919
|
-
const current =
|
|
3957
|
+
const current = _currentDocVideoState();
|
|
3920
3958
|
if (!STATE._acceptedVideoState) return current;
|
|
3921
3959
|
return _isVideoStateNewer(current, STATE._acceptedVideoState) ? current : STATE._acceptedVideoState;
|
|
3922
3960
|
}
|
|
@@ -4896,6 +4934,11 @@
|
|
|
4896
4934
|
yVideo.observe((event, transaction) => {
|
|
4897
4935
|
if (transaction.origin === 'local' || transaction.origin === 'local-repair') return;
|
|
4898
4936
|
const observedState = _readObservedVideoState(event);
|
|
4937
|
+
// Whoever sends room updates is clearly here: two devices that each missed the
|
|
4938
|
+
// other's presence note both thought they led and kept correcting each other.
|
|
4939
|
+
if (observedState?.sourceDeviceId && observedState.sourceDeviceId !== STATE.userId && yPresence?.has(observedState.sourceDeviceId)) {
|
|
4940
|
+
PRESENCE.seen.set(observedState.sourceDeviceId, Date.now());
|
|
4941
|
+
}
|
|
4899
4942
|
if (observedState && observedState.sourceDeviceId !== STATE.userId && Date.now() < STATE.localIntentUntil) {
|
|
4900
4943
|
// Someone else acted while our own action is still on its way. This used to be
|
|
4901
4944
|
// dropped for good — so two people pressing within a second kept different
|
|
@@ -4904,7 +4947,7 @@
|
|
|
4904
4947
|
clearTimeout(STATE._deferredObserve);
|
|
4905
4948
|
STATE._deferredObserve = setTimeout(() => {
|
|
4906
4949
|
if (!yVideo) return;
|
|
4907
|
-
_onObservedVideoState(
|
|
4950
|
+
_onObservedVideoState(_currentDocVideoState(), true);
|
|
4908
4951
|
}, Math.max(50, STATE.localIntentUntil - Date.now() + 60));
|
|
4909
4952
|
return;
|
|
4910
4953
|
}
|
|
@@ -5056,7 +5099,11 @@
|
|
|
5056
5099
|
const elapsed = Math.max(-wait / 1000, 0) * base;
|
|
5057
5100
|
const expected = Math.min(time + elapsed, video.duration || Infinity);
|
|
5058
5101
|
const drift = expected - video.currentTime;
|
|
5059
|
-
|
|
5102
|
+
// Heartbeat-driven jumps are spaced out: a device that stalls right after each jump
|
|
5103
|
+
// (slow network, heavy stream) would otherwise be yanked back again and again.
|
|
5104
|
+
const hbJumpOk = action !== 'heartbeat' || Date.now() - (STATE._hbSeekAt || 0) > 8000 || Math.abs(drift) > 15;
|
|
5105
|
+
if ((Math.abs(drift) > CONFIG.hardSyncTolerance && hbJumpOk) || (action !== 'heartbeat' && Math.abs(drift) > 0.6)) {
|
|
5106
|
+
if (action === 'heartbeat') STATE._hbSeekAt = Date.now();
|
|
5060
5107
|
remoteSeek(video, expected);
|
|
5061
5108
|
RATE.apply(video, base);
|
|
5062
5109
|
STATE.syncMode = 'TIGHT';
|
|
@@ -5126,6 +5173,7 @@
|
|
|
5126
5173
|
if (idle || isSyncLeader()) {
|
|
5127
5174
|
this.drift = idle ? null : 0;
|
|
5128
5175
|
this._ema = 0;
|
|
5176
|
+
if (this._steps) { this._steps = 0; if (!idle) RATE.apply(v, room ? room.rate : RATE.base(v)); }
|
|
5129
5177
|
return;
|
|
5130
5178
|
}
|
|
5131
5179
|
const expected = Math.min(room.time + (getSyncedTime() - room.timestamp) / 1000 * room.rate + this.offset(), (v.duration || Infinity) - 0.1);
|
|
@@ -5139,15 +5187,29 @@
|
|
|
5139
5187
|
STATE._lastRemoteApplyAt = Date.now();
|
|
5140
5188
|
remoteSeek(v, expected + 0.05);
|
|
5141
5189
|
RATE.apply(v, base);
|
|
5190
|
+
this._steps = 0;
|
|
5142
5191
|
}
|
|
5143
5192
|
return;
|
|
5144
5193
|
}
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5194
|
+
// Speed nudges come in a few fixed steps and are held for a while: changing the
|
|
5195
|
+
// speed every few hundred milliseconds made some players (Safari above all) hitch
|
|
5196
|
+
// and stutter, which looked like a play/pause loop.
|
|
5197
|
+
const step = IS_IOS ? 0.03 : 0.02;
|
|
5198
|
+
const maxSteps = IS_IOS ? 1 : 2;
|
|
5199
|
+
const holdMs = IS_IOS ? 3000 : 1500;
|
|
5200
|
+
const now = Date.now();
|
|
5201
|
+
let want = 0;
|
|
5202
|
+
if (Math.abs(this._ema) > this.TOLERANCE) want = Math.sign(this._ema) * Math.min(maxSteps, Math.max(1, Math.round(Math.abs(this._ema) / 0.12)));
|
|
5203
|
+
else if (this._steps && Math.abs(this._ema) > this.TOLERANCE / 2 && Math.sign(this._ema) === Math.sign(this._steps)) want = this._steps; // hysteresis
|
|
5204
|
+
if (want === this._steps) {
|
|
5205
|
+
if (want) RATE.apply(v, base, 1 + want * step, holdMs + 1000); // keep it going
|
|
5206
|
+
return;
|
|
5150
5207
|
}
|
|
5208
|
+
if (now - (this._stepAt || 0) < holdMs && want !== 0) return;
|
|
5209
|
+
this._steps = want;
|
|
5210
|
+
this._stepAt = now;
|
|
5211
|
+
if (want) RATE.apply(v, base, 1 + want * step, holdMs + 1000);
|
|
5212
|
+
else RATE.apply(v, base);
|
|
5151
5213
|
}
|
|
5152
5214
|
};
|
|
5153
5215
|
|
|
@@ -5397,7 +5459,8 @@
|
|
|
5397
5459
|
} catch (_) { }
|
|
5398
5460
|
}
|
|
5399
5461
|
fbRequest('DELETE', path).catch(() => { });
|
|
5400
|
-
if (best
|
|
5462
|
+
if (best) {
|
|
5463
|
+
STATE._serverClockAt = Date.now();
|
|
5401
5464
|
STATE.clockOffset = best.offset;
|
|
5402
5465
|
STATE.rttMs = best.rtt;
|
|
5403
5466
|
logDebug(`Clock synced to server (offset ${Math.round(best.offset)}ms, rtt ${best.rtt}ms).`, 'success');
|
|
@@ -5427,15 +5490,19 @@
|
|
|
5427
5490
|
if (!STATE.handshakeVerified || STATE.isViewer || STATE.isReadOnly) return false;
|
|
5428
5491
|
const ids = PRESENCE.ids().filter(id => id === STATE.userId || !STATE.presenceMap[id]?.viewer);
|
|
5429
5492
|
if (!ids.includes(STATE.userId)) ids.push(STATE.userId);
|
|
5430
|
-
|
|
5431
|
-
|
|
5493
|
+
// A device in the background (hidden tab, screen off) has throttled timers and often a
|
|
5494
|
+
// paused video: it does not lead while someone is actually watching.
|
|
5495
|
+
const awake = ids.filter(id => id === STATE.userId ? !document.hidden : !STATE.presenceMap[id]?.hid);
|
|
5496
|
+
const pool = awake.length ? awake : ids;
|
|
5497
|
+
pool.sort();
|
|
5498
|
+
return pool[0] === STATE.userId;
|
|
5432
5499
|
}
|
|
5433
5500
|
|
|
5434
5501
|
function startPresence() {
|
|
5435
5502
|
if (STATE._presenceTimer) return;
|
|
5436
5503
|
const beat = () => {
|
|
5437
5504
|
if (ydoc && yPresence && STATE.role !== 'IDLE' && !STATE.isReadOnly) {
|
|
5438
|
-
ydoc.transact(() => yPresence.set(STATE.userId, { ts: Date.now(), color: STATE.color, viewer: !!STATE.isViewer, nick: STATE.nickname || '', v: NXU_VERSION, r: NXU_RANK, sess: NXU_SESSION }), 'local');
|
|
5505
|
+
ydoc.transact(() => yPresence.set(STATE.userId, { ts: Date.now(), color: STATE.color, viewer: !!STATE.isViewer, nick: STATE.nickname || '', v: NXU_VERSION, r: NXU_RANK, sess: NXU_SESSION, hid: !!document.hidden }), 'local');
|
|
5439
5506
|
}
|
|
5440
5507
|
PRESENCE.refresh();
|
|
5441
5508
|
};
|
|
@@ -5473,6 +5540,7 @@
|
|
|
5473
5540
|
processedMsgIds.clear();
|
|
5474
5541
|
RTC.destroy();
|
|
5475
5542
|
STATE.clockOffset = 0;
|
|
5543
|
+
STATE._serverClockAt = 0;
|
|
5476
5544
|
STATE.rttMs = null;
|
|
5477
5545
|
releaseVideoHooks(STATE.video);
|
|
5478
5546
|
MEDIA_SESSION.release();
|
|
@@ -5600,6 +5668,11 @@
|
|
|
5600
5668
|
}
|
|
5601
5669
|
|
|
5602
5670
|
// ───────────────────────── Media helpers shared by chat, bookmarks and PiP
|
|
5671
|
+
function cinemaControlsMode() {
|
|
5672
|
+
if (STATE.controlsOverride) return STATE.controlsOverride;
|
|
5673
|
+
return STORE.get('nxu_cinema_controls', 'nexus') === 'site' ? 'site' : 'nexus';
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5603
5676
|
function currentMediaInfo() {
|
|
5604
5677
|
if (STATE.video) {
|
|
5605
5678
|
return { kind: 'video', el: STATE.video, time: STATE.video.currentTime, duration: STATE.video.duration, paused: STATE.video.paused };
|
|
@@ -6084,7 +6157,9 @@
|
|
|
6084
6157
|
|
|
6085
6158
|
function updateSmartPosition(wrapper, fab, dock, reactPill) {
|
|
6086
6159
|
if (!wrapper || !fab || !dock || !reactPill) return;
|
|
6087
|
-
|
|
6160
|
+
// In the container's own frame: screen rectangles are turned 90° in landscape view on
|
|
6161
|
+
// iPhone, which put the menu off screen.
|
|
6162
|
+
const rect = localBox(wrapper), w = getW(), h = getH();
|
|
6088
6163
|
const isLeft = rect.left + 24 < w / 2, isTop = rect.top + 24 < h / 2;
|
|
6089
6164
|
[dock, reactPill].forEach(el => { el.style.left = el.style.right = el.style.top = el.style.bottom = 'auto'; });
|
|
6090
6165
|
fab.style.top = '0px'; fab.style.left = '0px';
|
|
@@ -6125,7 +6200,13 @@
|
|
|
6125
6200
|
let probe = container.querySelector('.safe-probe');
|
|
6126
6201
|
if (!probe) { probe = createEl('div', 'safe-probe'); container.appendChild(probe); }
|
|
6127
6202
|
const cs = getComputedStyle(probe);
|
|
6128
|
-
|
|
6203
|
+
const env = { top: parseFloat(cs.paddingTop) || 0, right: parseFloat(cs.paddingRight) || 0, bottom: parseFloat(cs.paddingBottom) || 0, left: parseFloat(cs.paddingLeft) || 0 };
|
|
6204
|
+
// Landscape view on iPhone turns the UI, but the notch and home bar stay where they
|
|
6205
|
+
// are on the screen: map them into the UI's own frame.
|
|
6206
|
+
const deg = typeof ROTATE !== 'undefined' ? ROTATE.deg : 0;
|
|
6207
|
+
if (deg === 90) return { top: env.right, right: env.bottom, bottom: env.left, left: env.top };
|
|
6208
|
+
if (deg === -90) return { top: env.left, right: env.top, bottom: env.right, left: env.bottom };
|
|
6209
|
+
return env;
|
|
6129
6210
|
}
|
|
6130
6211
|
|
|
6131
6212
|
// Position of an element inside the NexusSync container, in the container's own frame.
|
|
@@ -6381,7 +6462,7 @@
|
|
|
6381
6462
|
// ───────────────────────── UI stylesheet
|
|
6382
6463
|
const UI_CSS = `
|
|
6383
6464
|
:host { all: initial; }
|
|
6384
|
-
.nexus-container { --txt:#fff; --glass:rgba(20,25,30,0.
|
|
6465
|
+
.nexus-container { --txt:#fff; --glass:rgba(20,25,30,0.34); --glass-strong:rgba(18,22,28,0.5); --accent:#00e676; --glass-active:rgba(0,230,118,0.28);
|
|
6385
6466
|
--sub-size:20px; --sub-original-size:13px; --sub-interim-size:17px; --sub-opacity:0.9; --sub-bg:rgba(8,10,14,0.82);
|
|
6386
6467
|
font-family: system-ui, -apple-system, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; font-size:13px; color:var(--txt);
|
|
6387
6468
|
container-type:size; container-name:nexus; width:100%; height:100%; position:absolute; inset:0; pointer-events:none; }
|
|
@@ -6408,7 +6489,7 @@
|
|
|
6408
6489
|
pushes past the menu edge (that briefly made a scrollbar appear). */
|
|
6409
6490
|
.dock .notify-pulse, .nexus-panel .notify-pulse { animation-name:pulseGlow; }
|
|
6410
6491
|
@keyframes pulseGlow { 0%,100% { box-shadow:0 0 6px rgba(0,230,118,0.35); filter:brightness(1); } 50% { box-shadow:0 0 16px rgba(0,230,118,0.85), inset 0 0 10px rgba(0,230,118,0.25); filter:brightness(1.18); } }
|
|
6411
|
-
#reaction-pill { position:absolute; background:var(--glass); backdrop-filter:blur(
|
|
6492
|
+
#reaction-pill { position:absolute; background:var(--glass); backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px); border:1px solid rgba(255,255,255,0.2); border-radius:24px; padding:0 4px; display:flex; align-items:center;
|
|
6412
6493
|
opacity:0; pointer-events:none; transition:opacity 0.3s, transform 0.3s; white-space:nowrap; z-index:95; height:48px; }
|
|
6413
6494
|
#reaction-pill.visible { opacity:1; pointer-events:auto; }
|
|
6414
6495
|
#reaction-pill { max-width:calc(100vw - 72px); max-width:calc(100cqw - 72px); overflow-x:auto; overflow-y:hidden; scrollbar-width:none; }
|
|
@@ -6427,7 +6508,7 @@
|
|
|
6427
6508
|
@media (prefers-reduced-motion: reduce) { .react-btn.pop, .react-btn.pop::after, .nx-spark { animation:none !important; } }
|
|
6428
6509
|
.presence-pill { position:absolute; font-size:11px; padding:3px 8px; border-radius:99px; background:rgba(0,0,0,0.6); color:#e0e0e0; white-space:nowrap; pointer-events:auto; }
|
|
6429
6510
|
.dock { position:absolute; background:var(--glass-strong); border-radius:16px; padding:10px; display:flex; flex-direction:column; gap:6px; pointer-events:none; opacity:0; border:1px solid rgba(255,255,255,0.1);
|
|
6430
|
-
transition:opacity 0.25s ease, transform 0.25s ease; box-shadow:0 10px 30px rgba(0,0,0,0.
|
|
6511
|
+
transition:opacity 0.25s ease, transform 0.25s ease; box-shadow:0 10px 30px rgba(0,0,0,0.35); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); z-index:90; overflow-y:auto; text-shadow:0 1px 2px rgba(0,0,0,0.8); overflow-x:hidden; width:clamp(150px, 40vw, 190px); width:clamp(150px, 40cqw, 190px); }
|
|
6431
6512
|
.dock.open { opacity:1; pointer-events:auto; border-color:rgba(0,230,118,0.3); }
|
|
6432
6513
|
.btn { background:rgba(255,255,255,0.06); color:var(--txt); border:1px solid transparent; padding:9px 12px; border-radius:9px; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px;
|
|
6433
6514
|
font-weight:600; font-size:13px; transition:background-color 0.2s, border-color 0.2s; touch-action:manipulation; width:100%; text-align:center; }
|
|
@@ -6525,8 +6606,10 @@
|
|
|
6525
6606
|
.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; }
|
|
6526
6607
|
.sync-bubble.visible { opacity:1; transform:translate(-50%,0); }
|
|
6527
6608
|
/* bubbles */
|
|
6609
|
+
.nexus-container.nx-composing .nx-bubbles { top:calc(70px + var(--nx-compose-h, 120px) + env(safe-area-inset-top, 0px)); }
|
|
6610
|
+
.nx-bubbles { transition:top 0.25s ease; }
|
|
6528
6611
|
.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; }
|
|
6529
|
-
.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.
|
|
6612
|
+
.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);
|
|
6530
6613
|
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; }
|
|
6531
6614
|
.nx-bubble.in { opacity:1; transform:none; }
|
|
6532
6615
|
.nx-bubble.out { opacity:0; transform:translateX(-24px); }
|
|
@@ -6535,7 +6618,7 @@
|
|
|
6535
6618
|
.nx-bubble-text { margin-top:3px; font-size:13.5px; line-height:1.4; overflow:hidden; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow-wrap:anywhere; }
|
|
6536
6619
|
/* talk: speech bubbles */
|
|
6537
6620
|
.nx-bubble.nx-talk { display:flex; align-items:flex-start; gap:9px; width:auto; max-width:100%; align-self:flex-start; border-left:none; border-radius:18px 18px 18px 6px; padding:8px 12px 9px 8px;
|
|
6538
|
-
background:linear-gradient(135deg, color-mix(in srgb, var(--c, #69f0ae) 22%, rgba(18,20,26,0.
|
|
6621
|
+
background:linear-gradient(135deg, color-mix(in srgb, var(--c, #69f0ae) 22%, rgba(18,20,26,0.58)), rgba(18,20,26,0.5)); box-shadow:0 8px 24px rgba(0,0,0,0.45), 0 0 0 1px color-mix(in srgb, var(--c, #69f0ae) 45%, transparent) inset; transform:translateX(-18px) scale(0.9); transform-origin:left bottom; transition:opacity 0.28s ease, transform 0.35s cubic-bezier(.2,1.4,.4,1); }
|
|
6539
6622
|
.nx-bubble.nx-talk.in { transform:none; }
|
|
6540
6623
|
.nx-bubble.nx-talk.out { opacity:0; transform:translateY(-8px) scale(0.92); transition:opacity 0.3s ease, transform 0.3s ease; }
|
|
6541
6624
|
.nx-bubble.nx-talk.mine { align-self:flex-start; border-radius:18px 18px 6px 18px; opacity:0; background:rgba(18,20,26,0.8); }
|
|
@@ -6551,17 +6634,46 @@
|
|
|
6551
6634
|
.nx-dots i:nth-child(2) { animation-delay:0.15s; } .nx-dots i:nth-child(3) { animation-delay:0.3s; }
|
|
6552
6635
|
@keyframes nxDot { 0%,60%,100% { transform:translateY(0); opacity:0.45; } 30% { transform:translateY(-4px); opacity:1; } }
|
|
6553
6636
|
@keyframes nxFadeIn { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:none; } }
|
|
6554
|
-
.nx-
|
|
6555
|
-
background:rgba(16,18,24,0.
|
|
6637
|
+
.nx-mode-card { position:absolute; left:50%; top:calc(64px + env(safe-area-inset-top, 0px)); transform:translate(-50%, -10px); z-index:2066; width:min(380px, calc(100% - 28px)); padding:11px 12px 11px; border-radius:16px;
|
|
6638
|
+
background:rgba(16,18,24,0.62); -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); box-shadow:0 12px 30px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.14) inset; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,0.8);
|
|
6639
|
+
opacity:0; transition:opacity 0.25s ease, transform 0.25s ease; pointer-events:auto; }
|
|
6640
|
+
.nx-mode-card.in { opacity:1; transform:translate(-50%, 0); }
|
|
6641
|
+
.nx-mode-head { display:flex; align-items:center; gap:7px; font-weight:800; font-size:14px; padding-right:26px; }
|
|
6642
|
+
.nx-mode-sub { margin-top:4px; font-size:12.5px; line-height:1.4; opacity:0.85; }
|
|
6643
|
+
.nx-mode-row { display:flex; gap:7px; margin-top:9px; flex-wrap:wrap; }
|
|
6644
|
+
.nx-mode-btn { flex:1 1 auto; border:1px solid rgba(255,255,255,0.2); background:rgba(255,255,255,0.1); color:#fff; border-radius:10px; padding:8px 10px; font-size:12.5px; font-weight:800; cursor:pointer; font-family:inherit; text-shadow:none; }
|
|
6645
|
+
.nx-mode-btn.primary { background:#69f0ae; border-color:#69f0ae; color:#061009; }
|
|
6646
|
+
.nx-mode-x { position:absolute; top:8px; right:8px; width:24px; height:24px; border:none; border-radius:50%; background:rgba(255,255,255,0.12); color:#fff; font-size:11px; cursor:pointer; }
|
|
6647
|
+
.nx-next { position:absolute; right:calc(18px + env(safe-area-inset-right, 0px)); bottom:calc(120px + env(safe-area-inset-bottom, 0px)); z-index:2065; display:flex; align-items:center; gap:10px; padding:8px 8px 8px 8px; border-radius:16px;
|
|
6648
|
+
background:rgba(16,18,24,0.6); -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px); box-shadow:0 10px 28px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.14) inset; color:#fff; max-width:min(360px, calc(100% - 36px));
|
|
6649
|
+
opacity:0; transform:translateY(12px) scale(0.97); transition:opacity 0.25s ease, transform 0.25s ease; pointer-events:auto; text-shadow:0 1px 2px rgba(0,0,0,0.8); }
|
|
6650
|
+
.nx-next.in { opacity:1; transform:none; }
|
|
6651
|
+
.nx-next-go { position:relative; flex:0 0 auto; width:46px; height:46px; border:none; border-radius:50%; background:#fff; color:#111; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:18px; text-shadow:none; }
|
|
6652
|
+
.nx-next-ring { position:absolute; inset:-4px; border-radius:50%; background:conic-gradient(#69f0ae calc(var(--p, 0) * 360deg), rgba(255,255,255,0.18) 0); -webkit-mask:radial-gradient(circle, transparent 25px, #000 26px); mask:radial-gradient(circle, transparent 25px, #000 26px); opacity:0; transition:opacity 0.2s; }
|
|
6653
|
+
.nx-next.counting .nx-next-ring { opacity:1; }
|
|
6654
|
+
.nx-next-ic { position:relative; }
|
|
6655
|
+
.nx-next-text { min-width:0; cursor:pointer; }
|
|
6656
|
+
.nx-next-k { font-size:11px; font-weight:800; opacity:0.75; text-transform:uppercase; letter-spacing:0.04em; }
|
|
6657
|
+
.nx-next-l { font-size:14px; font-weight:800; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
6658
|
+
.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; }
|
|
6659
|
+
/* At the top: subtitles live at the bottom of the picture, and on phones the keyboard covers the bottom too. */
|
|
6660
|
+
.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;
|
|
6661
|
+
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);
|
|
6556
6662
|
transition:opacity 0.22s ease, transform 0.25s cubic-bezier(.2,1.3,.4,1); display:flex; flex-direction:column; gap:6px; }
|
|
6557
6663
|
.nx-compose.open { opacity:1; transform:translate(-50%, 0) scale(1); pointer-events:auto; }
|
|
6558
6664
|
.nx-compose-head { display:flex; align-items:center; gap:6px; padding:0 2px 0 6px; }
|
|
6559
6665
|
.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; }
|
|
6560
6666
|
.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; }
|
|
6667
|
+
.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; }
|
|
6668
|
+
.nx-compose-sub:empty { display:none; }
|
|
6561
6669
|
.nx-compose-chips { display:flex; gap:6px; overflow-x:auto; padding:0 2px; }
|
|
6562
6670
|
.nx-compose-row { display:flex; gap:6px; }
|
|
6563
6671
|
.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; }
|
|
6564
6672
|
.nx-compose-input:focus { border-color:var(--c, #69f0ae); }
|
|
6673
|
+
.nx-compose-x.nx-compose-full { width:auto; padding:0 10px 0 8px; border-radius:999px; display:inline-flex; align-items:center; gap:5px; font-size:12px; font-weight:800; background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.18); }
|
|
6674
|
+
.nx-compose-x.nx-compose-full:hover { background:rgba(255,255,255,0.2); }
|
|
6675
|
+
.nx-hist-count:empty { display:none; }
|
|
6676
|
+
.nx-hist-count { min-width:16px; height:16px; padding:0 4px; border-radius:8px; background:var(--c, #69f0ae); color:#061009; font-size:10.5px; line-height:16px; text-align:center; }
|
|
6565
6677
|
.nx-compose-send { flex:0 0 auto; width:40px; height:40px; border:none; border-radius:50%; background:var(--c, #69f0ae); color:#061009; font-size:16px; font-weight:900; cursor:pointer; }
|
|
6566
6678
|
.nx-compose-send:active, .nx-compose-x:active { transform:scale(0.92); }
|
|
6567
6679
|
.cc-vhud { position:absolute; top:50%; right:calc(28px + env(safe-area-inset-right, 0px)); transform:translateY(-50%) scale(0.92); z-index:2070; display:flex; flex-direction:column; align-items:center; gap:8px;
|
|
@@ -6604,6 +6716,8 @@
|
|
|
6604
6716
|
.nx-latency-label { font-weight:700; }
|
|
6605
6717
|
.nx-latency-range { flex:1 1 140px; accent-color:var(--c, #69f0ae); }
|
|
6606
6718
|
.nx-latency-live { flex-basis:100%; font-size:11px; opacity:0.65; }
|
|
6719
|
+
.nx-talk-pending { opacity:0.45; animation:nxPend 1s ease-in-out infinite alternate; }
|
|
6720
|
+
@keyframes nxPend { from { opacity:0.25; } to { opacity:0.6; } }
|
|
6607
6721
|
.nx-talk-orig { margin-top:3px; font-size:11.5px; opacity:0.6; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:240px; }
|
|
6608
6722
|
.nx-quick-field { flex:1 1 auto; min-width:120px; padding:6px 12px; font-size:14px; }
|
|
6609
6723
|
.quick-edit { opacity:0.75; }
|
|
@@ -6677,6 +6791,8 @@
|
|
|
6677
6791
|
.rx { position:absolute; z-index:50; pointer-events:none !important; will-change:transform, opacity; line-height:1; user-select:none; -webkit-user-select:none; }
|
|
6678
6792
|
.rx-emoji { filter:drop-shadow(0 3px 6px rgba(0,0,0,0.45)) drop-shadow(0 0 10px rgba(255,255,255,0.18)); opacity:0; }
|
|
6679
6793
|
.rx-mini { opacity:0; filter:drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
|
|
6794
|
+
.rx-crumb { width:5px; height:5px; border-radius:2px; background:#c68a4e; box-shadow:0 0 0 1px rgba(90,50,20,0.35); opacity:0; }
|
|
6795
|
+
.rx-steam { width:6px; height:14px; border-radius:50%; background:radial-gradient(ellipse at center, rgba(255,255,255,0.85), rgba(255,255,255,0)); filter:blur(1.5px); opacity:0; }
|
|
6680
6796
|
.rx-spark { width:7px; height:7px; border-radius:50%; background:#fff; box-shadow:0 0 6px 2px var(--c, #69f0ae), 0 0 14px 4px var(--c, #69f0ae); opacity:0; }
|
|
6681
6797
|
.rx-ring { width:60px; height:60px; border-radius:50%; border:3px solid var(--c, #69f0ae); box-shadow:0 0 18px var(--c, #69f0ae), inset 0 0 12px var(--c, #69f0ae); }
|
|
6682
6798
|
.rx-glow { top:0 !important; bottom:0; width:min(90px, 14%); opacity:0; }
|
|
@@ -6705,8 +6821,9 @@
|
|
|
6705
6821
|
.cc-btn.cc-mode { gap:6px; padding:0 12px 0 10px; background:rgba(255,255,255,0.12); font-size:12px; }
|
|
6706
6822
|
.cc-btn.cc-mode:hover { background:rgba(255,255,255,0.22); }
|
|
6707
6823
|
.cc-mode-label { white-space:nowrap; }
|
|
6708
|
-
.cc-scrim { position:absolute; inset:0; z-index:59; pointer-events:none; background:
|
|
6709
|
-
|
|
6824
|
+
.cc-scrim { position:absolute; inset:0; z-index:59; pointer-events:none; background:transparent; transition:opacity 0.35s ease; }
|
|
6825
|
+
/* The picture stays fully clear while the controls show; the controls carry their own shadow. */
|
|
6826
|
+
.cinema-chrome .cc-top, .cinema-chrome .cc-bottom, .cinema-chrome .cc-center { filter:drop-shadow(0 1px 2px rgba(0,0,0,0.85)) drop-shadow(0 0 8px rgba(0,0,0,0.45)); }
|
|
6710
6827
|
.cc-top, .cc-bottom, .cc-center { position:absolute; left:0; right:0; z-index:75; pointer-events:auto; transition:opacity 0.3s ease, transform 0.3s ease; }
|
|
6711
6828
|
.cc-top { pointer-events:none; }
|
|
6712
6829
|
.cc-top .cc-btn { pointer-events:auto; }
|
|
@@ -6800,7 +6917,7 @@
|
|
|
6800
6917
|
.nexus-container.cinema-on .sub-overlay-container { transition:bottom 0.35s ease, opacity 0.3s ease; }
|
|
6801
6918
|
.cinema-chrome.touch .cc-back, .cinema-chrome.touch .cc-fwd, .cinema-chrome.touch .cc-play { display:none; }
|
|
6802
6919
|
.cinema-chrome.touch .cc-seek { height:28px; }
|
|
6803
|
-
@container nexus (max-width: 560px) { .cc-
|
|
6920
|
+
@container nexus (max-width: 560px) { .cc-fit, .cinema-chrome:not(.site-mode) .cc-mode-label { display:none; } .cc-btn.cc-mode { padding:0 8px; } .cc-btn { min-width:36px; padding:0 6px; } .cc-time { font-size:12px; margin-left:2px; } }
|
|
6804
6921
|
@container nexus (max-width: 380px) { .cc-ambient { display:none; } }
|
|
6805
6922
|
@container nexus (max-width: 600px) {
|
|
6806
6923
|
.dock { padding:8px; gap:5px; }
|
|
@@ -6886,6 +7003,8 @@
|
|
|
6886
7003
|
const btnSyncStatus = mk('nxu-sync-status', '📡 ' + t('sync'));
|
|
6887
7004
|
const btnInvite = mk('nxu-invite', '👥 ' + t('invite'), t('inviteTitle'));
|
|
6888
7005
|
const btnCinema = mk('nxu-cinema', '🍿 ' + t('cinema'), t('cinemaTitle'));
|
|
7006
|
+
const btnAutoCinema = mk('nxu-auto-cinema', '');
|
|
7007
|
+
btnAutoCinema.classList.add('subtle');
|
|
6889
7008
|
const btnPiP = mk('nxu-pip', '📺 PiP');
|
|
6890
7009
|
const btnRate = mk('nxu-rate', '⏩ 1x', t('rateTitle'));
|
|
6891
7010
|
// Talking happens in speech bubbles and quick ink now: the menu opens those directly
|
|
@@ -6998,7 +7117,8 @@
|
|
|
6998
7117
|
const toast = createEl('div', 'toast');
|
|
6999
7118
|
toast.setAttribute('role', 'status');
|
|
7000
7119
|
|
|
7001
|
-
|
|
7120
|
+
// 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);
|
|
7002
7122
|
uiWrapper.append(dock, reactPill, fab);
|
|
7003
7123
|
nexusContainer.append(drawCanvas, uiWrapper, chatPanel, subLogPanel, manualModal, transModal, toast, debugPanel);
|
|
7004
7124
|
shadow.appendChild(nexusContainer);
|
|
@@ -7009,7 +7129,7 @@
|
|
|
7009
7129
|
['keyup', 'keypress'].forEach(evt => nexusContainer.addEventListener(evt, e => { if (isEditableTarget(e.composedPath()[0])) e.stopPropagation(); }));
|
|
7010
7130
|
|
|
7011
7131
|
uiElements = {
|
|
7012
|
-
uiWrapper, fab, reactPill, dock, btnFindMedia, btnLink, btnSyncStatus, btnTranslate, btnCinema, btnPiP, btnRate, btnDraw, btnBookmark, btnChat, btnSubLog,
|
|
7132
|
+
uiWrapper, fab, reactPill, dock, btnFindMedia, btnLink, btnSyncStatus, btnTranslate, btnCinema, btnAutoCinema, btnPiP, btnRate, btnDraw, btnBookmark, btnChat, btnSubLog,
|
|
7013
7133
|
btnDebug, btnHideSite, chatPanel, chatMessages, chatInput, chatJumpBtn, subLogPanel, subLogMessages,
|
|
7014
7134
|
subLogJumpBtn, drawCanvas, manualModal, transModal, toast, debugJumpBtn, syncBubble: null,
|
|
7015
7135
|
pinError, guestInput, pinHint, pinDisplay, pinCopyBtn, hostBtn, connectBtn, btnInvite, typingRow, nickInput
|
|
@@ -7244,6 +7364,7 @@
|
|
|
7244
7364
|
};
|
|
7245
7365
|
|
|
7246
7366
|
btnCinema.onclick = () => { triggerHaptic(); CINEMA.toggle(); };
|
|
7367
|
+
btnAutoCinema.onclick = () => { triggerHaptic(); AUTO_CINEMA.setSite(!AUTO_CINEMA.enabled()); };
|
|
7247
7368
|
btnPiP.onclick = async () => {
|
|
7248
7369
|
let video = STATE.video;
|
|
7249
7370
|
if (!video || video.readyState === 0) {
|
|
@@ -7471,7 +7592,7 @@
|
|
|
7471
7592
|
},
|
|
7472
7593
|
publish() {
|
|
7473
7594
|
try {
|
|
7474
|
-
if (ydoc && yPresence && STATE.handshakeVerified) ydoc.transact(() => yPresence.set(STATE.userId, { ts: Date.now(), color: STATE.color, viewer: !!STATE.isViewer, nick: STATE.nickname || '', v: NXU_VERSION, r: NXU_RANK, sess: NXU_SESSION }), 'local');
|
|
7595
|
+
if (ydoc && yPresence && STATE.handshakeVerified) ydoc.transact(() => yPresence.set(STATE.userId, { ts: Date.now(), color: STATE.color, viewer: !!STATE.isViewer, nick: STATE.nickname || '', v: NXU_VERSION, r: NXU_RANK, sess: NXU_SESSION, hid: !!document.hidden }), 'local');
|
|
7475
7596
|
} catch (_) { }
|
|
7476
7597
|
},
|
|
7477
7598
|
// Small editor: name + colour swatches + any colour.
|
|
@@ -7587,25 +7708,43 @@
|
|
|
7587
7708
|
});
|
|
7588
7709
|
},
|
|
7589
7710
|
// → { text, from } or null when no translation is needed / possible.
|
|
7711
|
+
_pending: new Map(),
|
|
7590
7712
|
async translate(text) {
|
|
7591
7713
|
const tl = myLanguage();
|
|
7592
7714
|
const key = tl + '\n' + text;
|
|
7593
7715
|
if (this._cache.has(key)) return this._cache.get(key);
|
|
7594
7716
|
if (!/[\p{L}]{2,}/u.test(text)) return null; // emoji, numbers, "ㅋ"
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7717
|
+
// The bubble and the room chat ask for the same message at the same moment.
|
|
7718
|
+
if (this._pending.has(key)) return this._pending.get(key);
|
|
7719
|
+
const job = this._fetch(text, tl).then((result) => {
|
|
7720
|
+
// Only real answers are remembered: a network hiccup (slow iPad link, a
|
|
7721
|
+
// blocked request) must not leave that message untranslated for good.
|
|
7722
|
+
if (result !== undefined) {
|
|
7723
|
+
this._cache.set(key, result);
|
|
7724
|
+
if (this._cache.size > 200) this._cache.delete(this._cache.keys().next().value);
|
|
7725
|
+
}
|
|
7726
|
+
return result || null;
|
|
7727
|
+
}).finally(() => this._pending.delete(key));
|
|
7728
|
+
this._pending.set(key, job);
|
|
7729
|
+
return job;
|
|
7730
|
+
},
|
|
7731
|
+
// → { text, from } | null (no translation needed) | undefined (failed: try again later)
|
|
7732
|
+
async _fetch(text, tl) {
|
|
7733
|
+
const g = code => (code === 'zh' ? 'zh-CN' : code);
|
|
7734
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${g(tl)}&dt=t&q=${encodeURIComponent(text.slice(0, 500))}`;
|
|
7735
|
+
// Through the userscript manager first, then directly (some iPad setups only allow one).
|
|
7736
|
+
for (const viaFetch of [false, true]) {
|
|
7737
|
+
try {
|
|
7738
|
+
const res = await _httpRequest({ url, timeout: 9000, viaFetch });
|
|
7739
|
+
if (res.status !== 200) continue;
|
|
7600
7740
|
const data = JSON.parse(res.text);
|
|
7601
7741
|
const out = Array.isArray(data?.[0]) ? data[0].map(x => (x && x[0]) || '').join('').trim() : '';
|
|
7602
7742
|
const from = baseLang(String(data?.[2] || ''));
|
|
7603
|
-
if (out && from && from !== baseLang(tl) && out.toLowerCase() !== text.trim().toLowerCase())
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
return result;
|
|
7743
|
+
if (out && from && from !== baseLang(tl) && out.toLowerCase() !== text.trim().toLowerCase()) return { text: out, from };
|
|
7744
|
+
return null;
|
|
7745
|
+
} catch (_) { }
|
|
7746
|
+
}
|
|
7747
|
+
return undefined;
|
|
7609
7748
|
}
|
|
7610
7749
|
};
|
|
7611
7750
|
|
|
@@ -7635,17 +7774,21 @@
|
|
|
7635
7774
|
const reply = createEl('span', 'nx-talk-reply', '↩');
|
|
7636
7775
|
card.append(reply);
|
|
7637
7776
|
}
|
|
7638
|
-
|
|
7777
|
+
let timer = null, left = Math.max(4200, Math.min(11000, 3500 + text.length * 70)), startedAt = 0;
|
|
7778
|
+
if (!mine && CHAT_TR.enabled() && /[\p{L}]{2,}/u.test(text)) {
|
|
7779
|
+
const pend = createEl('span', 'nx-talk-orig nx-talk-pending', '🌐 …');
|
|
7780
|
+
body.querySelector('.nx-bubble-text').after(pend);
|
|
7639
7781
|
CHAT_TR.translate(text).then((tr) => {
|
|
7640
|
-
|
|
7782
|
+
pend.remove();
|
|
7783
|
+
if (!tr || !card.isConnected || card.classList.contains('out')) return;
|
|
7641
7784
|
const main = body.querySelector('.nx-bubble-text');
|
|
7642
7785
|
main.textContent = tr.text;
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
left
|
|
7786
|
+
main.after(createEl('span', 'nx-talk-orig', '🌐 ' + text));
|
|
7787
|
+
// Time to read the translation too (the bubble was already counting down).
|
|
7788
|
+
if (startedAt) { clearTimeout(timer); left = Math.max(left - (Date.now() - startedAt), 0) + 2500 + tr.text.length * 40; run(); }
|
|
7789
|
+
else left += 2500;
|
|
7646
7790
|
});
|
|
7647
7791
|
}
|
|
7648
|
-
let timer = null, left = Math.max(4200, Math.min(11000, 3500 + text.length * 70)), startedAt = 0;
|
|
7649
7792
|
const close = () => { clearTimeout(timer); card.classList.add('out'); setTimeout(() => card.remove(), 300); };
|
|
7650
7793
|
const run = () => { startedAt = Date.now(); timer = setTimeout(close, left); };
|
|
7651
7794
|
const hold = () => { clearTimeout(timer); left = Math.max(1500, left - (Date.now() - startedAt)); };
|
|
@@ -7687,7 +7830,8 @@
|
|
|
7687
7830
|
const to = createEl('span', 'nx-compose-to');
|
|
7688
7831
|
const x = createEl('button', 'nx-compose-x', '✕', { type: 'button', title: t('talkClose'), 'aria-label': t('talkClose') });
|
|
7689
7832
|
x.onclick = (e) => { e.stopPropagation(); this.close(); };
|
|
7690
|
-
const full = createEl('button', 'nx-compose-x nx-compose-full', '
|
|
7833
|
+
const full = createEl('button', 'nx-compose-x nx-compose-full', '', { type: 'button', title: t('talkFull'), 'aria-label': t('talkFull') });
|
|
7834
|
+
full.append(createEl('span', 'nx-hist-ic', '🗂'), createEl('span', 'nx-hist-label', t('talkHistory')), createEl('span', 'nx-hist-count'));
|
|
7691
7835
|
full.onclick = (e) => { e.stopPropagation(); this.close(); openChatPanel(); };
|
|
7692
7836
|
const me = createEl('button', 'nx-compose-x nx-me-dot', '', { type: 'button', title: t('profileTitle'), 'aria-label': t('profileTitle') });
|
|
7693
7837
|
me.style.setProperty('--c', STATE.color);
|
|
@@ -7700,6 +7844,8 @@
|
|
|
7700
7844
|
};
|
|
7701
7845
|
head.append(me, to, full, x);
|
|
7702
7846
|
const chips = createEl('div', 'nx-compose-chips');
|
|
7847
|
+
// The line being spoken right now, so writing a message never means missing it.
|
|
7848
|
+
const subEcho = createEl('div', 'nx-compose-sub');
|
|
7703
7849
|
const row = createEl('div', 'nx-compose-row');
|
|
7704
7850
|
const input = createEl('input', 'nx-compose-input', '', { type: 'text', maxlength: '500', placeholder: t('talkPlaceholder'), enterkeyhint: 'send', autocomplete: 'off' });
|
|
7705
7851
|
const send = createEl('button', 'nx-compose-send', '➤', { type: 'button', 'aria-label': t('talkSend') });
|
|
@@ -7711,7 +7857,7 @@
|
|
|
7711
7857
|
});
|
|
7712
7858
|
input.addEventListener('input', () => { SOCIAL.typing(); this._armClose(); });
|
|
7713
7859
|
row.append(input, send);
|
|
7714
|
-
wrap.append(head, chips, row);
|
|
7860
|
+
wrap.append(head, subEcho, chips, row);
|
|
7715
7861
|
['pointerdown', 'click', 'touchstart'].forEach(ev => wrap.addEventListener(ev, (e) => e.stopPropagation()));
|
|
7716
7862
|
container.appendChild(wrap);
|
|
7717
7863
|
this._composer = wrap;
|
|
@@ -7721,7 +7867,17 @@
|
|
|
7721
7867
|
const to = this._composer.querySelector('.nx-compose-to');
|
|
7722
7868
|
to.textContent = replyTo ? t('talkReplyTo', { name: replyTo }) : '💬 ' + t('talkTitle');
|
|
7723
7869
|
this._composer.style.setProperty('--c', color || STATE.color || '#69f0ae');
|
|
7870
|
+
// How much history the room chat holds (shown on the "History" pill).
|
|
7871
|
+
const n = (() => { try { return yChat ? yChat.length : 0; } catch (_) { return 0; } })();
|
|
7872
|
+
const cnt = this._composer.querySelector('.nx-hist-count');
|
|
7873
|
+
if (cnt) cnt.textContent = n ? (n > 99 ? '99+' : String(n)) : '';
|
|
7724
7874
|
this._composer.classList.add('open');
|
|
7875
|
+
clearInterval(this._subTimer);
|
|
7876
|
+
this._subTimer = setInterval(() => this._echoSubtitle(), 300);
|
|
7877
|
+
this._echoSubtitle();
|
|
7878
|
+
// The bubbles step down below the open box (both live at the top now).
|
|
7879
|
+
const host = getContainer();
|
|
7880
|
+
if (host) { host.classList.add('nx-composing'); requestAnimationFrame(() => host.style.setProperty('--nx-compose-h', (this._composer.offsetHeight || 120) + 'px')); }
|
|
7725
7881
|
this._fitKeyboard();
|
|
7726
7882
|
if (!this._vvBound && window.visualViewport) {
|
|
7727
7883
|
this._vvBound = true;
|
|
@@ -7753,9 +7909,23 @@
|
|
|
7753
7909
|
if (!this._composer) return;
|
|
7754
7910
|
if (this._composer.classList.contains('open')) UI_GUARD.shield();
|
|
7755
7911
|
this._composer.classList.remove('open');
|
|
7912
|
+
clearInterval(this._subTimer);
|
|
7913
|
+
getContainer()?.classList.remove('nx-composing');
|
|
7756
7914
|
try { this._input?.blur(); } catch (_) { }
|
|
7757
7915
|
},
|
|
7758
7916
|
isOpen() { return !!this._composer?.classList.contains('open'); },
|
|
7917
|
+
_echoSubtitle() {
|
|
7918
|
+
const box = this._composer?.querySelector('.nx-compose-sub');
|
|
7919
|
+
if (!box) return;
|
|
7920
|
+
if (!this.isOpen()) { clearInterval(this._subTimer); return; }
|
|
7921
|
+
let text = '';
|
|
7922
|
+
const finalSlot = shadow?.getElementById('nxu-final-slot');
|
|
7923
|
+
const last = finalSlot?.lastElementChild;
|
|
7924
|
+
if (last && TRANSLATOR.active) text = (last.querySelector('.sub-final-text')?.textContent || '' || '').trim();
|
|
7925
|
+
if (!text) { try { text = COVER._cueText(); } catch (_) { } }
|
|
7926
|
+
text = String(text || '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
7927
|
+
if (box.textContent !== (text ? '💬 ' + text : '')) box.textContent = text ? '💬 ' + text : '';
|
|
7928
|
+
},
|
|
7759
7929
|
// Quick replies in the reader's language; ✏️ edits them (saved on this device).
|
|
7760
7930
|
_renderChips() {
|
|
7761
7931
|
const chips = this._composer?.querySelector('.nx-compose-chips');
|
|
@@ -8035,6 +8205,259 @@
|
|
|
8035
8205
|
}
|
|
8036
8206
|
};
|
|
8037
8207
|
|
|
8208
|
+
// ───────────────────────── Automatic cinema
|
|
8209
|
+
// Opening a page that is all about one video (an episode, a movie — not a feed with many
|
|
8210
|
+
// posts) switches to cinema mode as soon as the video is ready. It keeps the site's own
|
|
8211
|
+
// player, because subtitles and servers are often picked there; a small card says which
|
|
8212
|
+
// player is in use and switches to NexusSync's player in one tap. Per site on/off.
|
|
8213
|
+
const AUTO_CINEMA = {
|
|
8214
|
+
_doneFor: '',
|
|
8215
|
+
_auto: false,
|
|
8216
|
+
_card: null,
|
|
8217
|
+
FEED_HOSTS: /(^|\.)(reddit\.com|x\.com|twitter\.com|facebook\.com|instagram\.com|tiktok\.com|threads\.net|9gag\.com|imgur\.com|tumblr\.com|pinterest\.[a-z.]+|linkedin\.com|discord\.com|dcinside\.com|fmkorea\.com|theqoo\.net|ruliweb\.com|clien\.net|naver\.com|daum\.net)$/i,
|
|
8218
|
+
_sites() { const v = STORE.get('nxu_auto_cinema_sites', {}); return v && typeof v === 'object' ? v : {}; },
|
|
8219
|
+
enabled() {
|
|
8220
|
+
const v = this._sites()[HOST];
|
|
8221
|
+
if (v === true || v === false) return v;
|
|
8222
|
+
return !this.FEED_HOSTS.test(HOST);
|
|
8223
|
+
},
|
|
8224
|
+
setSite(on) {
|
|
8225
|
+
const all = { ...this._sites(), [HOST]: !!on };
|
|
8226
|
+
const keys = Object.keys(all);
|
|
8227
|
+
if (keys.length > 300) delete all[keys[0]];
|
|
8228
|
+
STORE.set('nxu_auto_cinema_sites', all);
|
|
8229
|
+
this.paintButton();
|
|
8230
|
+
showToast(t(on ? 'autoCinemaOnSite' : 'autoCinemaOffSite', { site: HOST }));
|
|
8231
|
+
},
|
|
8232
|
+
paintButton() {
|
|
8233
|
+
const b = uiElements.btnAutoCinema;
|
|
8234
|
+
if (!b) return;
|
|
8235
|
+
const on = this.enabled();
|
|
8236
|
+
b.textContent = '✨ ' + t(on ? 'autoCinemaBtnOn' : 'autoCinemaBtnOff');
|
|
8237
|
+
b.classList.toggle('active', on);
|
|
8238
|
+
},
|
|
8239
|
+
// One real video on the page, big enough, long enough — not a feed.
|
|
8240
|
+
_singleMedia() {
|
|
8241
|
+
if (/\/shorts\//.test(location.pathname) || /\/reel(s)?\//.test(location.pathname)) return false;
|
|
8242
|
+
if (document.querySelectorAll('article').length >= 4) return false;
|
|
8243
|
+
const doc = document.scrollingElement || document.documentElement;
|
|
8244
|
+
if (doc && doc.scrollHeight > window.innerHeight * 10) return false;
|
|
8245
|
+
let big = 0;
|
|
8246
|
+
for (const v of document.querySelectorAll('video')) {
|
|
8247
|
+
const r = v.getBoundingClientRect();
|
|
8248
|
+
if (r.width >= 200 && r.height >= 110) big++;
|
|
8249
|
+
}
|
|
8250
|
+
for (const [ifr] of VIDEO_DETECTOR.frameReports) {
|
|
8251
|
+
const r = ifr.getBoundingClientRect();
|
|
8252
|
+
if (r.width >= 200 && r.height >= 110) big++;
|
|
8253
|
+
}
|
|
8254
|
+
if (big > 1) return false;
|
|
8255
|
+
const info = currentMediaInfo();
|
|
8256
|
+
if (!info) return false;
|
|
8257
|
+
const r = info.el.getBoundingClientRect();
|
|
8258
|
+
if (r.width < Math.min(420, window.innerWidth * 0.6)) return false;
|
|
8259
|
+
return Number.isFinite(info.duration) && info.duration >= 90;
|
|
8260
|
+
},
|
|
8261
|
+
tick() {
|
|
8262
|
+
if (!IS_TOP || STATE.retired || STATE.isCinema || STATE.pendingCinema || document.hidden) return;
|
|
8263
|
+
const key = location.href.split('#')[0];
|
|
8264
|
+
if (this._doneFor === key || !this.enabled() || STORE.get('nxu_hide_fab_' + HOST, false)) return;
|
|
8265
|
+
if (!(STATE.video || STATE.cinemaIframe)) return;
|
|
8266
|
+
if (STATE.video && STATE.video.readyState < 1) return;
|
|
8267
|
+
if (!this._singleMedia()) return;
|
|
8268
|
+
this._doneFor = key;
|
|
8269
|
+
this._auto = true;
|
|
8270
|
+
CINEMA.setControls('site', { save: false });
|
|
8271
|
+
CINEMA.set(true, { silent: true });
|
|
8272
|
+
if (!STATE.isCinema) { this._auto = false; STATE.controlsOverride = null; }
|
|
8273
|
+
},
|
|
8274
|
+
onEnter() {
|
|
8275
|
+
const auto = this._auto;
|
|
8276
|
+
this._auto = false;
|
|
8277
|
+
setTimeout(() => this._showCard(auto), 400);
|
|
8278
|
+
},
|
|
8279
|
+
onExit() {
|
|
8280
|
+
this._doneFor = location.href.split('#')[0]; // left on purpose: not again on this page
|
|
8281
|
+
this._hideCard();
|
|
8282
|
+
},
|
|
8283
|
+
_hideCard() {
|
|
8284
|
+
const c = this._card;
|
|
8285
|
+
this._card = null;
|
|
8286
|
+
clearTimeout(this._cardTimer);
|
|
8287
|
+
if (c) { c.classList.remove('in'); setTimeout(() => c.remove(), 260); }
|
|
8288
|
+
},
|
|
8289
|
+
// Which player is in use, with a one-tap switch (and the per-site switch after an automatic start).
|
|
8290
|
+
_showCard(auto) {
|
|
8291
|
+
if (!STATE.isCinema || !IS_TOP) return;
|
|
8292
|
+
const container = getContainer();
|
|
8293
|
+
if (!container) return;
|
|
8294
|
+
this._hideCard();
|
|
8295
|
+
const site = CINEMA_UI.controlsMode() === 'site';
|
|
8296
|
+
const card = createEl('div', 'nx-mode-card');
|
|
8297
|
+
const head = createEl('div', 'nx-mode-head');
|
|
8298
|
+
head.append(createEl('span', 'nx-mode-ic', site ? '🎛️' : '🍿'), createEl('span', 'nx-mode-title', t(site ? 'modeSitePlayer' : 'modeNexusPlayer')));
|
|
8299
|
+
const sub = createEl('div', 'nx-mode-sub', t(site ? (auto ? 'modeSiteHintAuto' : 'modeSiteHint') : 'modeNexusHint'));
|
|
8300
|
+
const row = createEl('div', 'nx-mode-row');
|
|
8301
|
+
const sw = createEl('button', 'nx-mode-btn primary', t(site ? 'modeUseNexus' : 'modeUseSite'), { type: 'button' });
|
|
8302
|
+
sw.onclick = (e) => {
|
|
8303
|
+
e.stopPropagation();
|
|
8304
|
+
const next = CINEMA_UI.controlsMode() === 'site' ? 'nexus' : 'site';
|
|
8305
|
+
CINEMA_UI._resetGestures?.();
|
|
8306
|
+
CINEMA.setControls(next, { save: false });
|
|
8307
|
+
CINEMA_UI.sync();
|
|
8308
|
+
this._hideCard();
|
|
8309
|
+
setTimeout(() => this._showCard(false), 350);
|
|
8310
|
+
};
|
|
8311
|
+
row.appendChild(sw);
|
|
8312
|
+
if (auto) {
|
|
8313
|
+
const off = createEl('button', 'nx-mode-btn', t('autoCinemaStopSite'), { type: 'button' });
|
|
8314
|
+
off.onclick = (e) => { e.stopPropagation(); this.setSite(false); this._hideCard(); CINEMA.set(false, { silent: true }); };
|
|
8315
|
+
row.appendChild(off);
|
|
8316
|
+
}
|
|
8317
|
+
const x = createEl('button', 'nx-mode-x', '✕', { type: 'button', 'aria-label': t('talkClose') });
|
|
8318
|
+
x.onclick = (e) => { e.stopPropagation(); this._hideCard(); };
|
|
8319
|
+
card.append(x, head, sub, row);
|
|
8320
|
+
['pointerdown', 'click', 'touchstart'].forEach(ev => card.addEventListener(ev, (e) => e.stopPropagation()));
|
|
8321
|
+
container.appendChild(card);
|
|
8322
|
+
this._card = card;
|
|
8323
|
+
requestAnimationFrame(() => card.classList.add('in'));
|
|
8324
|
+
this._cardTimer = setTimeout(() => this._hideCard(), auto ? 9000 : 4500);
|
|
8325
|
+
resetIdleTimer(true);
|
|
8326
|
+
}
|
|
8327
|
+
};
|
|
8328
|
+
setInterval(() => { try { AUTO_CINEMA.tick(); } catch (_) { } }, 1200);
|
|
8329
|
+
|
|
8330
|
+
// ───────────────────────── Next episode
|
|
8331
|
+
// On series pages (drama/anime sites) the page usually has a "next episode" link or
|
|
8332
|
+
// button. Near the end of an episode in cinema mode a small card offers it, and when the
|
|
8333
|
+
// episode ends it counts down and moves on (in a room: everyone is invited along, and
|
|
8334
|
+
// this device comes back into the same room on the next page).
|
|
8335
|
+
const NEXT_EP = {
|
|
8336
|
+
_card: null,
|
|
8337
|
+
_target: null,
|
|
8338
|
+
_scanAt: 0,
|
|
8339
|
+
_cancelledFor: '',
|
|
8340
|
+
_count: null,
|
|
8341
|
+
enabled() { return STORE.get('nxu_auto_next', true) !== false; },
|
|
8342
|
+
// Words that clearly mean "next episode" …
|
|
8343
|
+
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,
|
|
8344
|
+
// … and ones that only might ("다음", "Next", ⏭): they need a matching class/id too.
|
|
8345
|
+
RX_WEAK: /(^\s*다음\s*[▶>»]?\s*$|^\s*next\s*[▶>»]?\s*$|^\s*⏭\s*$|次へ)/i,
|
|
8346
|
+
RX_ATTR: /(next[-_ ]?(ep|episode|eps|video|chapter)|episode[-_]?next|ep[-_]?next|btn[-_]?next[-_]?ep)/i,
|
|
8347
|
+
RX_ATTR_WEAK: /(btn[-_]?next|nav[-_]?next|\bnext\b)/i,
|
|
8348
|
+
RX_BAD: /(prev|이전|previous|前|上一|comment|댓글|page|페이지|article|post|글|게시물|기사|뉴스|story|reply)/i,
|
|
8349
|
+
_visible(el) {
|
|
8350
|
+
if (!el.isConnected || el.closest('#nxu-host')) return false;
|
|
8351
|
+
const r = el.getBoundingClientRect();
|
|
8352
|
+
if (r.width < 6 || r.height < 6) return false;
|
|
8353
|
+
const cs = getComputedStyle(el);
|
|
8354
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && +cs.opacity > 0.05;
|
|
8355
|
+
},
|
|
8356
|
+
_label(el) {
|
|
8357
|
+
return [el.textContent, el.getAttribute('aria-label'), el.getAttribute('title'), el.getAttribute('alt')].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
8358
|
+
},
|
|
8359
|
+
// → { el, href, label } or null
|
|
8360
|
+
find() {
|
|
8361
|
+
const here = location.origin + location.pathname + location.search;
|
|
8362
|
+
const out = [];
|
|
8363
|
+
const rel = document.querySelector('a[rel~="next"][href], link[rel~="next"][href]');
|
|
8364
|
+
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 });
|
|
8365
|
+
const nodes = document.querySelectorAll('a[href], button, [role="button"], [onclick]');
|
|
8366
|
+
let n = 0;
|
|
8367
|
+
for (const el of nodes) {
|
|
8368
|
+
if (++n > 4000) break;
|
|
8369
|
+
const label = this._label(el);
|
|
8370
|
+
const attrs = `${el.id} ${typeof el.className === 'string' ? el.className : ''} ${el.getAttribute('data-action') || ''}`;
|
|
8371
|
+
let score = 0;
|
|
8372
|
+
if (this.RX_TEXT.test(label)) score += 60;
|
|
8373
|
+
else if (this.RX_WEAK.test(label)) score += 20;
|
|
8374
|
+
if (this.RX_ATTR.test(attrs)) score += 40;
|
|
8375
|
+
else if (this.RX_ATTR_WEAK.test(attrs)) score += 15;
|
|
8376
|
+
if (!score) continue;
|
|
8377
|
+
if (this.RX_BAD.test(label) || /prev|page|post|article|comment/i.test(attrs)) score -= 70;
|
|
8378
|
+
if (score <= 0 || !this._visible(el)) continue;
|
|
8379
|
+
const href = el.tagName === 'A' ? el.href : '';
|
|
8380
|
+
if (href) {
|
|
8381
|
+
if (/^javascript:/i.test(el.getAttribute('href') || '') || href.split('#')[0] === here) { if (!el.onclick && !el.hasAttribute('onclick')) continue; }
|
|
8382
|
+
else if (new URL(href).origin === location.origin) score += 10;
|
|
8383
|
+
}
|
|
8384
|
+
if (label.length > 60) score -= 15;
|
|
8385
|
+
out.push({ el, href, label, score });
|
|
8386
|
+
}
|
|
8387
|
+
out.sort((a, b) => b.score - a.score);
|
|
8388
|
+
const best = out[0];
|
|
8389
|
+
return best && best.score >= 40 ? best : null;
|
|
8390
|
+
},
|
|
8391
|
+
go(target = this._target) {
|
|
8392
|
+
if (!target) return;
|
|
8393
|
+
this.hide();
|
|
8394
|
+
showToast('⏭ ' + t('nextEpGoing'));
|
|
8395
|
+
const href = target.href && !/^javascript:/i.test(target.href) ? target.href : '';
|
|
8396
|
+
if (href && href.split('#')[0] !== location.href.split('#')[0]) {
|
|
8397
|
+
if (SOCIAL.live()) { TRAVEL.go(href); return; }
|
|
8398
|
+
location.href = href;
|
|
8399
|
+
return;
|
|
8400
|
+
}
|
|
8401
|
+
// A button that changes the episode with script: remember the room, then press it.
|
|
8402
|
+
try { TRAVEL.onLeavePage(); } catch (_) { }
|
|
8403
|
+
try { target.el?.click(); } catch (_) { }
|
|
8404
|
+
},
|
|
8405
|
+
hide() {
|
|
8406
|
+
clearInterval(this._count);
|
|
8407
|
+
this._count = null;
|
|
8408
|
+
const c = this._card;
|
|
8409
|
+
this._card = null;
|
|
8410
|
+
if (c) { c.classList.remove('in'); setTimeout(() => c.remove(), 260); }
|
|
8411
|
+
},
|
|
8412
|
+
_show(target, ended) {
|
|
8413
|
+
const container = getContainer();
|
|
8414
|
+
if (!container) return;
|
|
8415
|
+
if (!this._card || !this._card.isConnected) {
|
|
8416
|
+
const card = createEl('div', 'nx-next');
|
|
8417
|
+
const play = createEl('button', 'nx-next-go', '', { type: 'button' });
|
|
8418
|
+
play.append(createEl('span', 'nx-next-ring'), createEl('span', 'nx-next-ic', '⏭'));
|
|
8419
|
+
const text = createEl('div', 'nx-next-text');
|
|
8420
|
+
text.append(createEl('div', 'nx-next-k', t('nextEpTitle')), createEl('div', 'nx-next-l'));
|
|
8421
|
+
const cancel = createEl('button', 'nx-next-x', '✕', { type: 'button', title: t('nextEpCancel'), 'aria-label': t('nextEpCancel') });
|
|
8422
|
+
card.append(play, text, cancel);
|
|
8423
|
+
['pointerdown', 'click', 'touchstart'].forEach(ev => card.addEventListener(ev, (e) => e.stopPropagation()));
|
|
8424
|
+
play.onclick = () => this.go();
|
|
8425
|
+
text.onclick = () => this.go();
|
|
8426
|
+
cancel.onclick = () => { this._cancelledFor = location.href; this.hide(); UI_GUARD.shield(); };
|
|
8427
|
+
container.appendChild(card);
|
|
8428
|
+
this._card = card;
|
|
8429
|
+
requestAnimationFrame(() => card.classList.add('in'));
|
|
8430
|
+
}
|
|
8431
|
+
this._card.querySelector('.nx-next-l').textContent = target.label && target.label.length > 2 ? target.label : t('nextEpGo');
|
|
8432
|
+
if (ended && !this._count && this.enabled()) {
|
|
8433
|
+
let left = 8;
|
|
8434
|
+
const ring = this._card.querySelector('.nx-next-ring');
|
|
8435
|
+
this._card.classList.add('counting');
|
|
8436
|
+
ring.style.setProperty('--p', '0');
|
|
8437
|
+
ring.dataset.n = String(left);
|
|
8438
|
+
this._count = setInterval(() => {
|
|
8439
|
+
left--;
|
|
8440
|
+
ring.dataset.n = String(Math.max(0, left));
|
|
8441
|
+
ring.style.setProperty('--p', String((8 - left) / 8));
|
|
8442
|
+
if (left <= 0) { clearInterval(this._count); this._count = null; this.go(); }
|
|
8443
|
+
}, 1000);
|
|
8444
|
+
}
|
|
8445
|
+
},
|
|
8446
|
+
tick() {
|
|
8447
|
+
if (!IS_TOP || !STATE.isCinema) { if (this._card) this.hide(); return; }
|
|
8448
|
+
const info = currentMediaInfo();
|
|
8449
|
+
if (!info || !Number.isFinite(info.duration) || info.duration < 240) { if (this._card) this.hide(); return; }
|
|
8450
|
+
const left = info.duration - info.time;
|
|
8451
|
+
const ended = left <= 0.8 || (info.kind === 'video' && info.el.ended);
|
|
8452
|
+
if (left > 25 && !ended) { if (this._card) this.hide(); this._target = null; return; }
|
|
8453
|
+
if (this._cancelledFor === location.href) return;
|
|
8454
|
+
if (!this._target || Date.now() - this._scanAt > 5000) { this._scanAt = Date.now(); this._target = this.find(); }
|
|
8455
|
+
if (!this._target) return;
|
|
8456
|
+
this._show(this._target, ended);
|
|
8457
|
+
}
|
|
8458
|
+
};
|
|
8459
|
+
setInterval(() => { try { NEXT_EP.tick(); } catch (_) { } }, 1000);
|
|
8460
|
+
|
|
8038
8461
|
// ───────────────────────── Cover: hide the picture for a moment (for everyone)
|
|
8039
8462
|
// 🙈 (or H in cinema, Alt+Shift+H anywhere) lays a soft, nearly opaque curtain over the
|
|
8040
8463
|
// picture on every device in the room — for a violent or scary scene when children are
|
|
@@ -8679,7 +9102,7 @@
|
|
|
8679
9102
|
// Animations run on the compositor (Web Animations), with a hard cap on live nodes.
|
|
8680
9103
|
// Button feedback in the reaction bar: a springy pop, a ring and a few tiny copies of the
|
|
8681
9104
|
// emoji bursting out around the button (kept inside the bar's neighbourhood).
|
|
8682
|
-
const PRESS_COLORS = { '
|
|
9105
|
+
const PRESS_COLORS = { '😂': '#ffd54f', '😭': '#82b1ff', '😡': '#ff5252', '💦': '#4fc3f7', '🍪': '#d7a86e', '☕': '#bcaaa4', '😘': '#ff6fae', '🖕': '#ffb74d' };
|
|
8683
9106
|
function pressFx(btn, emoji) {
|
|
8684
9107
|
if (!btn) return;
|
|
8685
9108
|
btn.style.setProperty('--pop', PRESS_COLORS[emoji] || '#69f0ae');
|
|
@@ -8715,9 +9138,10 @@
|
|
|
8715
9138
|
_combo: new Map(), // lane → { n, timer, badge }
|
|
8716
9139
|
_tags: new Map(), // uid → last name-tag time
|
|
8717
9140
|
reduced() { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (_) { return false; } },
|
|
8718
|
-
_node(cls, text, x, y, size) {
|
|
9141
|
+
_node(cls, text, x, y, size, force = false) {
|
|
8719
9142
|
const container = getContainer();
|
|
8720
|
-
|
|
9143
|
+
// The burst's ring and glow always get a place, even when many small bits are flying.
|
|
9144
|
+
if (!container || (this.live >= this.MAX && !force)) return null;
|
|
8721
9145
|
const el = createEl('div', 'rx ' + cls, text);
|
|
8722
9146
|
el.style.left = x + 'px';
|
|
8723
9147
|
el.style.top = y + 'px';
|
|
@@ -8772,7 +9196,7 @@
|
|
|
8772
9196
|
const sway = (Math.random() < 0.5 ? -1 : 1) * (10 + Math.random() * 14);
|
|
8773
9197
|
const d = 1700 + Math.random() * 500;
|
|
8774
9198
|
const base = { duration: d, delay };
|
|
8775
|
-
const kind = { '❤️': 'heart', '💗': 'heart', '😂': 'laugh', '😭': 'cry', '😡': 'angry', '🫣': 'peek', '🌰': 'toss', '🚽': 'flush' }[e] || 'float';
|
|
9199
|
+
const kind = { '❤️': 'heart', '💗': 'heart', '😂': 'laugh', '😭': 'cry', '😡': 'angry', '💦': 'splash', '🍪': 'munch', '☕': 'steam', '😘': 'kiss', '🖕': 'flip', '🫣': 'peek', '🌰': 'toss', '🚽': 'flush' }[e] || 'float';
|
|
8776
9200
|
if (this.reduced()) { this._run(el, [{ opacity: 0 }, { opacity: 1, offset: 0.2 }, { opacity: 0, transform: `translateY(${-R * 0.4}px)` }], base); return; }
|
|
8777
9201
|
switch (kind) {
|
|
8778
9202
|
case 'heart':
|
|
@@ -8822,6 +9246,101 @@
|
|
|
8822
9246
|
{ const puff = this._node('rx-mini', '💢', x + size * 0.7, lane.y - R * 0.3, 16);
|
|
8823
9247
|
this._run(puff, [{ transform: 'scale(0.3)', opacity: 0 }, { transform: 'scale(1.3)', opacity: 1, offset: 0.4 }, { transform: 'translateY(-30px) scale(0.6)', opacity: 0 }], { duration: 800, delay: delay + 300 }); }
|
|
8824
9248
|
break;
|
|
9249
|
+
case 'splash': {
|
|
9250
|
+
// Shoots up with a little shiver, then bursts into droplets.
|
|
9251
|
+
this._run(el, [
|
|
9252
|
+
{ transform: 'translate(0,0) scale(0.3) rotate(0)', opacity: 0 },
|
|
9253
|
+
{ transform: `translate(0, ${-R * 0.42}px) scale(1.25) rotate(-6deg)`, opacity: 1, offset: 0.3 },
|
|
9254
|
+
{ transform: `translate(3px, ${-R * 0.46}px) scale(1.15) rotate(6deg)`, offset: 0.38 },
|
|
9255
|
+
{ transform: `translate(-3px, ${-R * 0.5}px) scale(1.2) rotate(-4deg)`, offset: 0.46 },
|
|
9256
|
+
{ transform: `translate(0, ${-R * 0.55}px) scale(1.5)`, opacity: 0, offset: 0.6 },
|
|
9257
|
+
{ transform: `translate(0, ${-R * 0.55}px) scale(1.5)`, opacity: 0 }
|
|
9258
|
+
], { ...base, duration: 1500 });
|
|
9259
|
+
for (let i = 0; i < 6; i++) {
|
|
9260
|
+
const ang = -Math.PI / 2 + (i - 2.5) * 0.42;
|
|
9261
|
+
const dist = 40 + Math.random() * 34;
|
|
9262
|
+
const drop = this._node('rx-mini', '💧', x + size * 0.35, lane.y - R * 0.55, 13 + Math.random() * 5);
|
|
9263
|
+
this._run(drop, [
|
|
9264
|
+
{ transform: 'translate(0,0) scale(0.4)', opacity: 0 },
|
|
9265
|
+
{ transform: `translate(${Math.cos(ang) * dist}px, ${Math.sin(ang) * dist}px) scale(1)`, opacity: 1, offset: 0.4 },
|
|
9266
|
+
{ transform: `translate(${Math.cos(ang) * dist * 1.3}px, ${Math.sin(ang) * dist + 70}px) scale(0.7)`, opacity: 0 }
|
|
9267
|
+
], { duration: 900, delay: delay + 850, easing: 'cubic-bezier(.2,.6,.6,1)' });
|
|
9268
|
+
}
|
|
9269
|
+
break;
|
|
9270
|
+
}
|
|
9271
|
+
case 'munch': {
|
|
9272
|
+
// Rises, gets two quick bites (squash), crumbs fall.
|
|
9273
|
+
this._run(el, [
|
|
9274
|
+
{ transform: 'translate(0,0) scale(0.4) rotate(-20deg)', opacity: 0 },
|
|
9275
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.4}px) scale(1.15) rotate(8deg)`, opacity: 1, offset: 0.28 },
|
|
9276
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.44}px) scale(1.25, 0.85) rotate(0)`, offset: 0.36 },
|
|
9277
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.47}px) scale(1.1) rotate(-4deg)`, offset: 0.44 },
|
|
9278
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.5}px) scale(1.25, 0.85)`, offset: 0.52 },
|
|
9279
|
+
{ transform: `translate(${-sway * 0.3}px, ${-R * 0.85}px) scale(0.9) rotate(10deg)`, opacity: 0 }
|
|
9280
|
+
], base);
|
|
9281
|
+
for (let i = 0; i < 5; i++) {
|
|
9282
|
+
const crumb = this._node('rx-crumb', '', x + size * (0.25 + Math.random() * 0.5), lane.y - R * 0.45, 0);
|
|
9283
|
+
this._run(crumb, [
|
|
9284
|
+
{ transform: 'translate(0,0) scale(0.6)', opacity: 0 },
|
|
9285
|
+
{ transform: `translate(${(Math.random() - 0.5) * 30}px, 8px) scale(1)`, opacity: 1, offset: 0.2 },
|
|
9286
|
+
{ transform: `translate(${(Math.random() - 0.5) * 50}px, ${50 + Math.random() * 40}px) rotate(${Math.random() * 180}deg)`, opacity: 0 }
|
|
9287
|
+
], { duration: 800, delay: delay + d * 0.34 + i * 60, easing: 'ease-in' });
|
|
9288
|
+
}
|
|
9289
|
+
break;
|
|
9290
|
+
}
|
|
9291
|
+
case 'steam': {
|
|
9292
|
+
// A calm cup: slow rise, gentle sway, steam curling up.
|
|
9293
|
+
this._run(el, [
|
|
9294
|
+
{ transform: 'translate(0,0) scale(0.6)', opacity: 0 },
|
|
9295
|
+
{ transform: `translate(${sway * 0.3}px, ${-R * 0.25}px) scale(1.05)`, opacity: 1, offset: 0.25 },
|
|
9296
|
+
{ transform: `translate(${-sway * 0.3}px, ${-R * 0.45}px) scale(1.05)`, offset: 0.55 },
|
|
9297
|
+
{ transform: `translate(${sway * 0.2}px, ${-R * 0.6}px) scale(0.95)`, opacity: 0 }
|
|
9298
|
+
], { ...base, duration: 2400, easing: 'ease-in-out' });
|
|
9299
|
+
for (let i = 0; i < 3; i++) {
|
|
9300
|
+
const wisp = this._node('rx-steam', '', x + size * (0.3 + i * 0.18), lane.y - R * 0.3 - size * 0.6, 0);
|
|
9301
|
+
this._run(wisp, [
|
|
9302
|
+
{ transform: 'translate(0,0) scale(0.6, 0.8)', opacity: 0 },
|
|
9303
|
+
{ transform: `translate(${i % 2 ? 6 : -6}px, -22px) scale(1, 1.2)`, opacity: 0.8, offset: 0.4 },
|
|
9304
|
+
{ transform: `translate(${i % 2 ? -4 : 4}px, -52px) scale(1.4, 1.6)`, opacity: 0 }
|
|
9305
|
+
], { duration: 1500, delay: delay + 500 + i * 220, easing: 'ease-out' });
|
|
9306
|
+
}
|
|
9307
|
+
break;
|
|
9308
|
+
}
|
|
9309
|
+
case 'kiss': {
|
|
9310
|
+
// Leans in, then blows a kiss that floats toward the middle of the screen.
|
|
9311
|
+
const toward = lane.x > lane.W / 2 ? -1 : 1;
|
|
9312
|
+
this._run(el, [
|
|
9313
|
+
{ transform: 'translate(0,0) scale(0.4) rotate(0)', opacity: 0 },
|
|
9314
|
+
{ transform: `translate(0, ${-R * 0.35}px) scale(1.2) rotate(${toward * 12}deg)`, opacity: 1, offset: 0.3 },
|
|
9315
|
+
{ transform: `translate(${toward * 6}px, ${-R * 0.38}px) scale(1.05, 1.15) rotate(${toward * 18}deg)`, offset: 0.45 },
|
|
9316
|
+
{ transform: `translate(0, ${-R * 0.55}px) scale(0.9) rotate(0)`, opacity: 0 }
|
|
9317
|
+
], base);
|
|
9318
|
+
for (let i = 0; i < 2; i++) {
|
|
9319
|
+
const heart = this._node('rx-mini', i ? '💗' : '💋', x + size * 0.5, lane.y - R * 0.38, i ? 15 : 18);
|
|
9320
|
+
this._run(heart, [
|
|
9321
|
+
{ transform: 'translate(0,0) scale(0.3)', opacity: 0 },
|
|
9322
|
+
{ transform: `translate(${toward * 40}px, -16px) scale(1.15)`, opacity: 1, offset: 0.3 },
|
|
9323
|
+
{ transform: `translate(${toward * 110}px, -40px) scale(1)`, offset: 0.7 },
|
|
9324
|
+
{ transform: `translate(${toward * 150}px, -60px) scale(0.6)`, opacity: 0 }
|
|
9325
|
+
], { duration: 1300, delay: delay + d * 0.42 + i * 180, easing: 'ease-out' });
|
|
9326
|
+
}
|
|
9327
|
+
break;
|
|
9328
|
+
}
|
|
9329
|
+
case 'flip':
|
|
9330
|
+
// Cheeky: pops up, two jabs toward the screen, a shake, gone.
|
|
9331
|
+
this._run(el, [
|
|
9332
|
+
{ transform: 'translate(0,0) scale(0.3)', opacity: 0 },
|
|
9333
|
+
{ transform: `translate(0, ${-R * 0.3}px) scale(1.1)`, opacity: 1, offset: 0.18 },
|
|
9334
|
+
{ transform: `translate(0, ${-R * 0.33}px) scale(1.45)`, offset: 0.26 },
|
|
9335
|
+
{ transform: `translate(0, ${-R * 0.34}px) scale(1.1)`, offset: 0.33 },
|
|
9336
|
+
{ transform: `translate(0, ${-R * 0.36}px) scale(1.45)`, offset: 0.41 },
|
|
9337
|
+
{ transform: `translate(-4px, ${-R * 0.38}px) scale(1.15) rotate(-8deg)`, offset: 0.5 },
|
|
9338
|
+
{ transform: `translate(4px, ${-R * 0.4}px) scale(1.15) rotate(8deg)`, offset: 0.57 },
|
|
9339
|
+
{ transform: `translate(0, ${-R * 0.7}px) scale(0.9)`, opacity: 0 }
|
|
9340
|
+
], { ...base, easing: 'ease-out' });
|
|
9341
|
+
{ const pop = this._node('rx-mini', '💥', x + size * 0.6, lane.y - R * 0.36, 16);
|
|
9342
|
+
this._run(pop, [{ transform: 'scale(0.2)', opacity: 0 }, { transform: 'scale(1.3)', opacity: 1, offset: 0.35 }, { transform: 'scale(0.6) translateY(-16px)', opacity: 0 }], { duration: 600, delay: delay + d * 0.25 }); }
|
|
9343
|
+
break;
|
|
8825
9344
|
case 'peek':
|
|
8826
9345
|
this._run(el, [
|
|
8827
9346
|
{ transform: `translate(${lane.x > lane.W / 2 ? 60 : -60}px, ${-R * 0.3}px) scale(1)`, opacity: 0 },
|
|
@@ -8883,12 +9402,12 @@
|
|
|
8883
9402
|
_burst(e, side, color) {
|
|
8884
9403
|
const lane = this._lane(side);
|
|
8885
9404
|
const ox = side === 'right' ? lane.W - 30 : 30, oy = lane.y + 20;
|
|
8886
|
-
const ring = this._node('rx-ring', '', ox, oy);
|
|
9405
|
+
const ring = this._node('rx-ring', '', ox, oy, 0, true);
|
|
8887
9406
|
if (ring) {
|
|
8888
9407
|
ring.style.setProperty('--c', color);
|
|
8889
9408
|
this._run(ring, [{ transform: 'translate(-50%,-50%) scale(0.2)', opacity: 0.9 }, { transform: 'translate(-50%,-50%) scale(4.2)', opacity: 0 }], { duration: 900, easing: 'ease-out' });
|
|
8890
9409
|
}
|
|
8891
|
-
const glow = this._node('rx-glow ' + side, '', 0, 0);
|
|
9410
|
+
const glow = this._node('rx-glow ' + side, '', 0, 0, 0, true);
|
|
8892
9411
|
if (glow) {
|
|
8893
9412
|
glow.style.setProperty('--c', color);
|
|
8894
9413
|
this._run(glow, [{ opacity: 0 }, { opacity: 1, offset: 0.25 }, { opacity: 0 }], { duration: 1100 });
|
|
@@ -9148,7 +9667,7 @@
|
|
|
9148
9667
|
_scrubbing: false,
|
|
9149
9668
|
_wasPaused: null,
|
|
9150
9669
|
|
|
9151
|
-
controlsMode() { return
|
|
9670
|
+
controlsMode() { return cinemaControlsMode(); },
|
|
9152
9671
|
|
|
9153
9672
|
_setIcon(el, name, size = 22, label = '') {
|
|
9154
9673
|
const key = `${name}|${size}|${label}`;
|
|
@@ -9225,7 +9744,7 @@
|
|
|
9225
9744
|
const subBtn = btn('cc-sub', 'cc', t('aiSubTitle'));
|
|
9226
9745
|
const fitBtn = btn('cc-fit', 'fit', t('ccFit'));
|
|
9227
9746
|
const ambientBtn = btn('cc-ambient', 'ambient', t('ccAmbient'));
|
|
9228
|
-
const pipBtn = btn('cc-pip', 'pip', '
|
|
9747
|
+
const pipBtn = btn('cc-pip', 'pip', t('ccPip'));
|
|
9229
9748
|
const fsBtn = btn('cc-fs', 'fullscreen', t('ccFullscreen'));
|
|
9230
9749
|
row.append(playBtn, backBtn, fwdBtn, vol, time, live, spacer, rateBtn, subBtn, fitBtn, ambientBtn, pipBtn, fsBtn);
|
|
9231
9750
|
bottom.append(seek, row);
|
|
@@ -10537,6 +11056,7 @@
|
|
|
10537
11056
|
if (uiElements.btnRate) uiElements.btnRate.style.display = hasMedia ? '' : 'none';
|
|
10538
11057
|
updateRateButton();
|
|
10539
11058
|
if (uiElements.btnCinema) uiElements.btnCinema.style.display = hasMedia || STATE.isCinema ? '' : 'none';
|
|
11059
|
+
if (uiElements.btnAutoCinema) { uiElements.btnAutoCinema.style.display = hasMedia || STATE.isCinema ? '' : 'none'; AUTO_CINEMA.paintButton(); }
|
|
10540
11060
|
if (uiElements.btnBookmark) uiElements.btnBookmark.style.display = hasMedia ? '' : 'none';
|
|
10541
11061
|
if (uiElements.btnFindMedia) uiElements.btnFindMedia.style.display = hasMedia ? 'none' : '';
|
|
10542
11062
|
if (uiElements.btnLink) uiElements.btnLink.style.display = hasMedia || connected ? '' : 'none';
|
|
@@ -10587,7 +11107,7 @@
|
|
|
10587
11107
|
initFirebaseStreaming(STATE.handshakeVerified ? 'nexus_rooms' : (NETWORK.streamNode || 'nexus_rooms'),
|
|
10588
11108
|
STATE.handshakeVerified ? STATE.topicId : (NETWORK.streamNode === 'nexus_lobbies' ? STATE.lobbyId : STATE.topicId));
|
|
10589
11109
|
if (STATE.handshakeVerified) {
|
|
10590
|
-
try { if (ydoc && yPresence) ydoc.transact(() => yPresence.set(STATE.userId, { ts: Date.now(), color: STATE.color, viewer: !!STATE.isViewer, nick: STATE.nickname || '', v: NXU_VERSION, r: NXU_RANK, sess: NXU_SESSION }), 'local'); } catch (_) { }
|
|
11110
|
+
try { if (ydoc && yPresence) ydoc.transact(() => yPresence.set(STATE.userId, { ts: Date.now(), color: STATE.color, viewer: !!STATE.isViewer, nick: STATE.nickname || '', v: NXU_VERSION, r: NXU_RANK, sess: NXU_SESSION, hid: !!document.hidden }), 'local'); } catch (_) { }
|
|
10591
11111
|
sendFirebaseCmd('heartbeat', { ts: Date.now() }, 'nexus_rooms');
|
|
10592
11112
|
syncServerClock();
|
|
10593
11113
|
if (RTC.isHost && STATE.connectionMode === 'HTTP_RELAY') RTC.init();
|
|
@@ -10596,6 +11116,8 @@
|
|
|
10596
11116
|
window.addEventListener('pageshow', (e) => { if (e.persisted) { DEVICE_GUARD.onVisible(60000); reviveConnection('page restore'); } });
|
|
10597
11117
|
window.addEventListener('pagehide', () => DEVICE_GUARD.onHidden());
|
|
10598
11118
|
document.addEventListener('visibilitychange', () => {
|
|
11119
|
+
// Tell the room at once whether this device is in the foreground (leadership).
|
|
11120
|
+
if (IS_TOP) try { PROFILE.publish(); } catch (_) { }
|
|
10599
11121
|
if (document.hidden) { _hiddenSince = Date.now(); DEVICE_GUARD.onHidden(); return; }
|
|
10600
11122
|
const away = _hiddenSince ? Date.now() - _hiddenSince : 0;
|
|
10601
11123
|
_hiddenSince = 0;
|
package/package.json
CHANGED