@kangsicj/nexussync 1.0.1 → 1.1.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 +1 -1
- package/dist/NexusSync.user.js +528 -66
- 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// @icon https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/assets/icon.svg
|
|
10
10
|
// @updateURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.meta.js
|
|
11
11
|
// @downloadURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.user.js
|
|
12
|
-
// @version 1.0
|
|
12
|
+
// @version 1.1.0
|
|
13
13
|
// @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
14
|
// @description:ko 어떤 웹 영상이든 완벽한 싱크로 함께 보기: 시네마 모드, 실시간 AI 자막·번역, 자동 번역 말풍선 채팅, 잠깐 그리기, 가리기, 다음 화도 함께.
|
|
15
15
|
// @match *://*/*
|
package/dist/NexusSync.user.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// @icon https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/assets/icon.svg
|
|
10
10
|
// @updateURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.meta.js
|
|
11
11
|
// @downloadURL https://cdn.jsdelivr.net/npm/@kangsicj/nexussync@latest/dist/NexusSync.user.js
|
|
12
|
-
// @version 1.0
|
|
12
|
+
// @version 1.1.0
|
|
13
13
|
// @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
14
|
// @description:ko 어떤 웹 영상이든 완벽한 싱크로 함께 보기: 시네마 모드, 실시간 AI 자막·번역, 자동 번역 말풍선 채팅, 잠깐 그리기, 가리기, 다음 화도 함께.
|
|
15
15
|
// @match *://*/*
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
|
|
41
41
|
// The version of the code that is actually running (the manager's metadata can lag
|
|
42
42
|
// behind when an old copy is still installed).
|
|
43
|
-
const NXU_VERSION = '1.0
|
|
43
|
+
const NXU_VERSION = '1.1.0';
|
|
44
44
|
// Rank used by the one-instance guard below and to tell newer builds apart in a room.
|
|
45
45
|
// It is independent of the public version (1.0.0 is the first public release, after
|
|
46
46
|
// private builds that reached ranks up to 10.2.0), so every older copy still installed
|
|
47
47
|
// steps aside instead of this release.
|
|
48
|
-
const NXU_RANK = '11.0
|
|
48
|
+
const NXU_RANK = '11.1.0';
|
|
49
49
|
const NXU_SESSION = Math.random().toString(36).slice(2, 10); // this page load // raise with every release
|
|
50
50
|
// App icon (same artwork as assets/icon.svg / @icon), inlined so it shows offline too.
|
|
51
51
|
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 +171,7 @@
|
|
|
171
171
|
'nxu_idle_timeout', 'nxu_sub_size', 'nxu_sub_opacity', 'nxu_sub_position', 'nxu_speaker_language',
|
|
172
172
|
'nxu_subtitle_language', 'nxu_audio_mode', 'nxu_translation_provider', 'nxu_stt_local_first',
|
|
173
173
|
'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
|
|
174
|
+
'nxu_model_openai', 'nxu_model_anthropic', 'nxu_cinema_ambient', 'nxu_cinema_fit', 'nxu_cc_remaining', 'nxu_cinema_controls', 'nxu_nickname', 'nxu_color', 'nxu_quick_replies', 'nxu_chat_translate', 'nxu_sync_offset', 'nxu_cover_band', 'nxu_cover_marks', 'nxu_travel', 'nxu_auto_next', 'nxu_auto_cinema_sites', 'nxu_glossary', 'nxu_panel_size_chat-panel', 'nxu_panel_size_sub-log-panel', 'nxu_panel_size_debug-panel', 'nxu_panel_size_trans-modal', 'nxu_cinema_rotate', 'nxu_rotate_denied_' + HOST
|
|
175
175
|
]);
|
|
176
176
|
|
|
177
177
|
// ───────────────────────── Cross-origin HTTP (GM xhr first, fetch fallback)
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
hardSyncTolerance: 2.5,
|
|
241
241
|
softSyncTolerance: 0.25,
|
|
242
242
|
syncHeartbeatMs: 3000,
|
|
243
|
-
emojis: ['
|
|
243
|
+
emojis: ['😂', '😭', '😡', '💦', '🍪', '☕', '😘', '🖕'],
|
|
244
244
|
chunkSize: 2000,
|
|
245
245
|
relayBatchInterval: 250,
|
|
246
246
|
relayBatchIntervalTight: 80,
|
|
@@ -382,7 +382,8 @@
|
|
|
382
382
|
videoHookController: null,
|
|
383
383
|
hostHeartbeatTimer: null,
|
|
384
384
|
_presenceTimer: null,
|
|
385
|
-
|
|
385
|
+
// Rises across page loads too (a reloaded page keeps its identity when it rejoins).
|
|
386
|
+
_videoSequence: Date.now() - 1700000000000,
|
|
386
387
|
_lastVideoSeqBySource: new Map(),
|
|
387
388
|
_processedVideoEvents: new Map(),
|
|
388
389
|
_acceptedVideoState: null,
|
|
@@ -621,8 +622,8 @@
|
|
|
621
622
|
travelBack: 'Back in the room', travelMoved: 'Moved to “{title}” · tap to go together', travelGoing: 'Going together…',
|
|
622
623
|
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
624
|
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
|
|
625
|
+
modeSitePlayer: 'Site player in use', modeNexusPlayer: 'NexusSync player in use', modeSiteHint: 'Change subtitles or the server in the site’s player, then switch.', modeSiteHintAuto: 'Cinema started by itself. Pick subtitles or a server in the site’s player first if needed, then switch.', modeNexusHint: 'Everything is controlled by NexusSync. The site’s own settings are one tap away.', modeUseNexus: 'Use NexusSync player', modeUseSite: 'Use site player', autoCinemaStopSite: 'No auto-cinema on this site', autoCinemaOnSite: 'Auto-cinema on for {site}', autoCinemaOffSite: 'Auto-cinema off for {site}', autoCinemaBtnOn: 'Auto-cinema: on (this site)', autoCinemaBtnOff: 'Auto-cinema: off (this site)', ccPip: 'Picture-in-Picture (keep watching in a small window)', nextEpTitle: 'Up next', nextEpGo: 'Next episode', nextEpCancel: 'Stay on this episode', nextEpGoing: 'Next episode…', talkMenu: 'Chat', inkMenu: 'Quick sketch', quickEdit: 'Edit quick replies', quickEditHint: 'Separate with |', quickReset: 'Back to the default replies', save: 'Save',
|
|
626
|
+
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
627
|
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
628
|
you: 'You', roomBuffering: '⏳ {name} is loading… everyone waits', roomResume: '▶️ Everyone is ready — playing together', roomRate: '⏩ Speed {rate}x synced',
|
|
628
629
|
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 +705,8 @@
|
|
|
704
705
|
travelBack: '방에 다시 연결했어요', travelMoved: '「{title}」(으)로 이동했어요 · 눌러서 같이 가기', travelGoing: '같이 이동하는 중…',
|
|
705
706
|
chatTrOn: '자동 번역 켜짐', chatTrOff: '자동 번역 꺼짐', chatTrShowOrig: '눌러서 원문 보기', chatTrShowTr: '눌러서 번역 보기', chatTrFrom: '{lang}에서 번역됨',
|
|
706
707
|
inkClosing: '곧 닫혀요 — 계속 그리려면 움직이거나 눌러 주세요',
|
|
707
|
-
talkMenu: '말풍선 채팅', inkMenu: '잠깐 그리기', quickEdit: '빠른 답장 편집', quickEditHint: '| 로 구분', quickReset: '기본 답장으로', save: '저장',
|
|
708
|
-
talkTitle: '말풍선 채팅', talkPlaceholder: '메시지 보내기…', talkReplyTo: '↩ {name} 님에게 답장', talkClose: '닫기', talkSend: '보내기', talkFull: '전체
|
|
708
|
+
modeSitePlayer: '사이트 플레이어 사용 중', modeNexusPlayer: 'NexusSync 플레이어 사용 중', modeSiteHint: '자막이나 서버는 사이트 플레이어에서 바꾼 뒤 전환하세요.', modeSiteHintAuto: '시네마 모드가 자동으로 켜졌어요. 필요하면 사이트 플레이어에서 자막·서버를 먼저 고른 뒤 전환하세요.', modeNexusHint: 'NexusSync가 모든 조작을 맡아요. 사이트 설정은 한 번 탭으로 돌아갈 수 있어요.', modeUseNexus: 'NexusSync 플레이어로', modeUseSite: '사이트 플레이어로', autoCinemaStopSite: '이 사이트는 자동 시네마 끄기', autoCinemaOnSite: '{site}에서 자동 시네마 켜짐', autoCinemaOffSite: '{site}에서 자동 시네마 꺼짐', autoCinemaBtnOn: '자동 시네마: 켬 (이 사이트)', autoCinemaBtnOff: '자동 시네마: 끔 (이 사이트)', ccPip: 'PiP (작은 창으로 계속 보기)', nextEpTitle: '다음 화', nextEpGo: '다음 화 보기', nextEpCancel: '이번 화에 머물기', nextEpGoing: '다음 화로 이동하는 중…', talkMenu: '말풍선 채팅', inkMenu: '잠깐 그리기', quickEdit: '빠른 답장 편집', quickEditHint: '| 로 구분', quickReset: '기본 답장으로', save: '저장',
|
|
709
|
+
talkTitle: '말풍선 채팅', talkPlaceholder: '메시지 보내기…', talkReplyTo: '↩ {name} 님에게 답장', talkClose: '닫기', talkSend: '보내기', talkFull: '지금까지의 대화 기록 전체 보기', talkHistory: '대화 기록',
|
|
709
710
|
inkTitle: '잠깐 그리기 (몇 초 뒤 사라짐)', inkHint: '화면에 그려 보세요 — 몇 초 뒤 사라져요', inkDone: '완료', inkBubble: '님이 화면에 그리고 있어요 — 탭해서 같이 그리기',
|
|
710
711
|
you: '나', roomBuffering: '⏳ {name} 기기 로딩 중… 모두 잠시 대기', roomResume: '▶️ 모두 준비됨 — 함께 재생합니다', roomRate: '⏩ {rate}배속 동기화',
|
|
711
712
|
youBuffering: '⏳ 이 기기 영상 로딩 중 — 방이 기다려 줍니다', shieldResumeTitle: '탭해서 다시 재생', shieldResumeText: '브라우저가 이 영상을 멈췄습니다. 한 번 탭하면 방과 맞춰 이어서 재생합니다.',
|
|
@@ -1198,7 +1199,7 @@
|
|
|
1198
1199
|
return v === 'cover' || v === 'fill' ? v : 'contain';
|
|
1199
1200
|
},
|
|
1200
1201
|
// Cinema controls mode: the site's own player UI is hidden so only one set shows.
|
|
1201
|
-
nexusControls() { return IS_TOP ?
|
|
1202
|
+
nexusControls() { return IS_TOP ? cinemaControlsMode() !== 'site' : this._frameNexus === true; },
|
|
1202
1203
|
_startAmbient(video, root) {
|
|
1203
1204
|
this._stopAmbient();
|
|
1204
1205
|
if (!video || !root || !this.ambientEnabled()) return;
|
|
@@ -1379,8 +1380,12 @@
|
|
|
1379
1380
|
else this._frameFit = fit;
|
|
1380
1381
|
this._restyle();
|
|
1381
1382
|
},
|
|
1382
|
-
setControls(mode) {
|
|
1383
|
-
|
|
1383
|
+
setControls(mode, { save = true } = {}) {
|
|
1384
|
+
// save:false (automatic cinema) uses the site's player for this visit only.
|
|
1385
|
+
if (IS_TOP) {
|
|
1386
|
+
if (save) { STATE.controlsOverride = null; STORE.set('nxu_cinema_controls', mode === 'site' ? 'site' : 'nexus'); }
|
|
1387
|
+
else STATE.controlsOverride = mode === 'site' ? 'site' : 'nexus';
|
|
1388
|
+
}
|
|
1384
1389
|
this._restyle();
|
|
1385
1390
|
},
|
|
1386
1391
|
|
|
@@ -1489,6 +1494,8 @@
|
|
|
1489
1494
|
try { (document.exitFullscreen || document.webkitExitFullscreen)?.call(document)?.catch?.(() => { }); } catch (_) { }
|
|
1490
1495
|
}
|
|
1491
1496
|
if (!on) this._ownFullscreen = false;
|
|
1497
|
+
if (!on) { STATE.controlsOverride = null; AUTO_CINEMA.onExit(); }
|
|
1498
|
+
if (on) AUTO_CINEMA.onEnter();
|
|
1492
1499
|
if (!on && ROTATE.deg) ROTATE._tick(); // leaving cinema also leaves landscape view at once
|
|
1493
1500
|
CINEMA_UI.sync();
|
|
1494
1501
|
updateMediaButtons();
|
|
@@ -1836,7 +1843,9 @@
|
|
|
1836
1843
|
},
|
|
1837
1844
|
_decide(side) {
|
|
1838
1845
|
if (side) this._lastSide = side;
|
|
1839
|
-
|
|
1846
|
+
// Turned on with ↻: stays in landscape, but follows the phone to the other side
|
|
1847
|
+
// (left or right edge down) instead of staying stuck on the first one.
|
|
1848
|
+
if (this.manual) { if (side && this.deg && side !== this.deg) this._apply(side); return; }
|
|
1840
1849
|
if (!side) {
|
|
1841
1850
|
this.suppress = false;
|
|
1842
1851
|
if (this.deg) this._leave();
|
|
@@ -3638,7 +3647,9 @@
|
|
|
3638
3647
|
} else if (msg.t === 'pong') {
|
|
3639
3648
|
const t3 = Date.now();
|
|
3640
3649
|
const rtt = t3 - msg.t1;
|
|
3641
|
-
|
|
3650
|
+
// Everyone shares the server clock; the direct link only stands in until
|
|
3651
|
+
// this device has its own server measurement.
|
|
3652
|
+
if (!STATE._serverClockAt) STATE.clockOffset = msg.t2 - (msg.t1 + rtt / 2);
|
|
3642
3653
|
STATE.rttMs = rtt;
|
|
3643
3654
|
updateRttHUD(rtt);
|
|
3644
3655
|
logDebug(`P2P Clock Offset: ${STATE.clockOffset.toFixed(2)}ms`, "success");
|
|
@@ -4896,6 +4907,11 @@
|
|
|
4896
4907
|
yVideo.observe((event, transaction) => {
|
|
4897
4908
|
if (transaction.origin === 'local' || transaction.origin === 'local-repair') return;
|
|
4898
4909
|
const observedState = _readObservedVideoState(event);
|
|
4910
|
+
// Whoever sends room updates is clearly here: two devices that each missed the
|
|
4911
|
+
// other's presence note both thought they led and kept correcting each other.
|
|
4912
|
+
if (observedState?.sourceDeviceId && observedState.sourceDeviceId !== STATE.userId && yPresence?.has(observedState.sourceDeviceId)) {
|
|
4913
|
+
PRESENCE.seen.set(observedState.sourceDeviceId, Date.now());
|
|
4914
|
+
}
|
|
4899
4915
|
if (observedState && observedState.sourceDeviceId !== STATE.userId && Date.now() < STATE.localIntentUntil) {
|
|
4900
4916
|
// Someone else acted while our own action is still on its way. This used to be
|
|
4901
4917
|
// dropped for good — so two people pressing within a second kept different
|
|
@@ -5056,7 +5072,11 @@
|
|
|
5056
5072
|
const elapsed = Math.max(-wait / 1000, 0) * base;
|
|
5057
5073
|
const expected = Math.min(time + elapsed, video.duration || Infinity);
|
|
5058
5074
|
const drift = expected - video.currentTime;
|
|
5059
|
-
|
|
5075
|
+
// Heartbeat-driven jumps are spaced out: a device that stalls right after each jump
|
|
5076
|
+
// (slow network, heavy stream) would otherwise be yanked back again and again.
|
|
5077
|
+
const hbJumpOk = action !== 'heartbeat' || Date.now() - (STATE._hbSeekAt || 0) > 8000 || Math.abs(drift) > 15;
|
|
5078
|
+
if ((Math.abs(drift) > CONFIG.hardSyncTolerance && hbJumpOk) || (action !== 'heartbeat' && Math.abs(drift) > 0.6)) {
|
|
5079
|
+
if (action === 'heartbeat') STATE._hbSeekAt = Date.now();
|
|
5060
5080
|
remoteSeek(video, expected);
|
|
5061
5081
|
RATE.apply(video, base);
|
|
5062
5082
|
STATE.syncMode = 'TIGHT';
|
|
@@ -5126,6 +5146,7 @@
|
|
|
5126
5146
|
if (idle || isSyncLeader()) {
|
|
5127
5147
|
this.drift = idle ? null : 0;
|
|
5128
5148
|
this._ema = 0;
|
|
5149
|
+
if (this._steps) { this._steps = 0; if (!idle) RATE.apply(v, room ? room.rate : RATE.base(v)); }
|
|
5129
5150
|
return;
|
|
5130
5151
|
}
|
|
5131
5152
|
const expected = Math.min(room.time + (getSyncedTime() - room.timestamp) / 1000 * room.rate + this.offset(), (v.duration || Infinity) - 0.1);
|
|
@@ -5139,15 +5160,29 @@
|
|
|
5139
5160
|
STATE._lastRemoteApplyAt = Date.now();
|
|
5140
5161
|
remoteSeek(v, expected + 0.05);
|
|
5141
5162
|
RATE.apply(v, base);
|
|
5163
|
+
this._steps = 0;
|
|
5142
5164
|
}
|
|
5143
5165
|
return;
|
|
5144
5166
|
}
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5167
|
+
// Speed nudges come in a few fixed steps and are held for a while: changing the
|
|
5168
|
+
// speed every few hundred milliseconds made some players (Safari above all) hitch
|
|
5169
|
+
// and stutter, which looked like a play/pause loop.
|
|
5170
|
+
const step = IS_IOS ? 0.03 : 0.02;
|
|
5171
|
+
const maxSteps = IS_IOS ? 1 : 2;
|
|
5172
|
+
const holdMs = IS_IOS ? 3000 : 1500;
|
|
5173
|
+
const now = Date.now();
|
|
5174
|
+
let want = 0;
|
|
5175
|
+
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)));
|
|
5176
|
+
else if (this._steps && Math.abs(this._ema) > this.TOLERANCE / 2 && Math.sign(this._ema) === Math.sign(this._steps)) want = this._steps; // hysteresis
|
|
5177
|
+
if (want === this._steps) {
|
|
5178
|
+
if (want) RATE.apply(v, base, 1 + want * step, holdMs + 1000); // keep it going
|
|
5179
|
+
return;
|
|
5150
5180
|
}
|
|
5181
|
+
if (now - (this._stepAt || 0) < holdMs && want !== 0) return;
|
|
5182
|
+
this._steps = want;
|
|
5183
|
+
this._stepAt = now;
|
|
5184
|
+
if (want) RATE.apply(v, base, 1 + want * step, holdMs + 1000);
|
|
5185
|
+
else RATE.apply(v, base);
|
|
5151
5186
|
}
|
|
5152
5187
|
};
|
|
5153
5188
|
|
|
@@ -5397,7 +5432,8 @@
|
|
|
5397
5432
|
} catch (_) { }
|
|
5398
5433
|
}
|
|
5399
5434
|
fbRequest('DELETE', path).catch(() => { });
|
|
5400
|
-
if (best
|
|
5435
|
+
if (best) {
|
|
5436
|
+
STATE._serverClockAt = Date.now();
|
|
5401
5437
|
STATE.clockOffset = best.offset;
|
|
5402
5438
|
STATE.rttMs = best.rtt;
|
|
5403
5439
|
logDebug(`Clock synced to server (offset ${Math.round(best.offset)}ms, rtt ${best.rtt}ms).`, 'success');
|
|
@@ -5427,15 +5463,19 @@
|
|
|
5427
5463
|
if (!STATE.handshakeVerified || STATE.isViewer || STATE.isReadOnly) return false;
|
|
5428
5464
|
const ids = PRESENCE.ids().filter(id => id === STATE.userId || !STATE.presenceMap[id]?.viewer);
|
|
5429
5465
|
if (!ids.includes(STATE.userId)) ids.push(STATE.userId);
|
|
5430
|
-
|
|
5431
|
-
|
|
5466
|
+
// A device in the background (hidden tab, screen off) has throttled timers and often a
|
|
5467
|
+
// paused video: it does not lead while someone is actually watching.
|
|
5468
|
+
const awake = ids.filter(id => id === STATE.userId ? !document.hidden : !STATE.presenceMap[id]?.hid);
|
|
5469
|
+
const pool = awake.length ? awake : ids;
|
|
5470
|
+
pool.sort();
|
|
5471
|
+
return pool[0] === STATE.userId;
|
|
5432
5472
|
}
|
|
5433
5473
|
|
|
5434
5474
|
function startPresence() {
|
|
5435
5475
|
if (STATE._presenceTimer) return;
|
|
5436
5476
|
const beat = () => {
|
|
5437
5477
|
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');
|
|
5478
|
+
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
5479
|
}
|
|
5440
5480
|
PRESENCE.refresh();
|
|
5441
5481
|
};
|
|
@@ -5473,6 +5513,7 @@
|
|
|
5473
5513
|
processedMsgIds.clear();
|
|
5474
5514
|
RTC.destroy();
|
|
5475
5515
|
STATE.clockOffset = 0;
|
|
5516
|
+
STATE._serverClockAt = 0;
|
|
5476
5517
|
STATE.rttMs = null;
|
|
5477
5518
|
releaseVideoHooks(STATE.video);
|
|
5478
5519
|
MEDIA_SESSION.release();
|
|
@@ -5600,6 +5641,11 @@
|
|
|
5600
5641
|
}
|
|
5601
5642
|
|
|
5602
5643
|
// ───────────────────────── Media helpers shared by chat, bookmarks and PiP
|
|
5644
|
+
function cinemaControlsMode() {
|
|
5645
|
+
if (STATE.controlsOverride) return STATE.controlsOverride;
|
|
5646
|
+
return STORE.get('nxu_cinema_controls', 'nexus') === 'site' ? 'site' : 'nexus';
|
|
5647
|
+
}
|
|
5648
|
+
|
|
5603
5649
|
function currentMediaInfo() {
|
|
5604
5650
|
if (STATE.video) {
|
|
5605
5651
|
return { kind: 'video', el: STATE.video, time: STATE.video.currentTime, duration: STATE.video.duration, paused: STATE.video.paused };
|
|
@@ -6084,7 +6130,9 @@
|
|
|
6084
6130
|
|
|
6085
6131
|
function updateSmartPosition(wrapper, fab, dock, reactPill) {
|
|
6086
6132
|
if (!wrapper || !fab || !dock || !reactPill) return;
|
|
6087
|
-
|
|
6133
|
+
// In the container's own frame: screen rectangles are turned 90° in landscape view on
|
|
6134
|
+
// iPhone, which put the menu off screen.
|
|
6135
|
+
const rect = localBox(wrapper), w = getW(), h = getH();
|
|
6088
6136
|
const isLeft = rect.left + 24 < w / 2, isTop = rect.top + 24 < h / 2;
|
|
6089
6137
|
[dock, reactPill].forEach(el => { el.style.left = el.style.right = el.style.top = el.style.bottom = 'auto'; });
|
|
6090
6138
|
fab.style.top = '0px'; fab.style.left = '0px';
|
|
@@ -6125,7 +6173,13 @@
|
|
|
6125
6173
|
let probe = container.querySelector('.safe-probe');
|
|
6126
6174
|
if (!probe) { probe = createEl('div', 'safe-probe'); container.appendChild(probe); }
|
|
6127
6175
|
const cs = getComputedStyle(probe);
|
|
6128
|
-
|
|
6176
|
+
const env = { top: parseFloat(cs.paddingTop) || 0, right: parseFloat(cs.paddingRight) || 0, bottom: parseFloat(cs.paddingBottom) || 0, left: parseFloat(cs.paddingLeft) || 0 };
|
|
6177
|
+
// Landscape view on iPhone turns the UI, but the notch and home bar stay where they
|
|
6178
|
+
// are on the screen: map them into the UI's own frame.
|
|
6179
|
+
const deg = typeof ROTATE !== 'undefined' ? ROTATE.deg : 0;
|
|
6180
|
+
if (deg === 90) return { top: env.right, right: env.bottom, bottom: env.left, left: env.top };
|
|
6181
|
+
if (deg === -90) return { top: env.left, right: env.top, bottom: env.right, left: env.bottom };
|
|
6182
|
+
return env;
|
|
6129
6183
|
}
|
|
6130
6184
|
|
|
6131
6185
|
// Position of an element inside the NexusSync container, in the container's own frame.
|
|
@@ -6381,7 +6435,7 @@
|
|
|
6381
6435
|
// ───────────────────────── UI stylesheet
|
|
6382
6436
|
const UI_CSS = `
|
|
6383
6437
|
:host { all: initial; }
|
|
6384
|
-
.nexus-container { --txt:#fff; --glass:rgba(20,25,30,0.
|
|
6438
|
+
.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
6439
|
--sub-size:20px; --sub-original-size:13px; --sub-interim-size:17px; --sub-opacity:0.9; --sub-bg:rgba(8,10,14,0.82);
|
|
6386
6440
|
font-family: system-ui, -apple-system, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; font-size:13px; color:var(--txt);
|
|
6387
6441
|
container-type:size; container-name:nexus; width:100%; height:100%; position:absolute; inset:0; pointer-events:none; }
|
|
@@ -6408,7 +6462,7 @@
|
|
|
6408
6462
|
pushes past the menu edge (that briefly made a scrollbar appear). */
|
|
6409
6463
|
.dock .notify-pulse, .nexus-panel .notify-pulse { animation-name:pulseGlow; }
|
|
6410
6464
|
@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(
|
|
6465
|
+
#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
6466
|
opacity:0; pointer-events:none; transition:opacity 0.3s, transform 0.3s; white-space:nowrap; z-index:95; height:48px; }
|
|
6413
6467
|
#reaction-pill.visible { opacity:1; pointer-events:auto; }
|
|
6414
6468
|
#reaction-pill { max-width:calc(100vw - 72px); max-width:calc(100cqw - 72px); overflow-x:auto; overflow-y:hidden; scrollbar-width:none; }
|
|
@@ -6427,7 +6481,7 @@
|
|
|
6427
6481
|
@media (prefers-reduced-motion: reduce) { .react-btn.pop, .react-btn.pop::after, .nx-spark { animation:none !important; } }
|
|
6428
6482
|
.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
6483
|
.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.
|
|
6484
|
+
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
6485
|
.dock.open { opacity:1; pointer-events:auto; border-color:rgba(0,230,118,0.3); }
|
|
6432
6486
|
.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
6487
|
font-weight:600; font-size:13px; transition:background-color 0.2s, border-color 0.2s; touch-action:manipulation; width:100%; text-align:center; }
|
|
@@ -6526,7 +6580,7 @@
|
|
|
6526
6580
|
.sync-bubble.visible { opacity:1; transform:translate(-50%,0); }
|
|
6527
6581
|
/* bubbles */
|
|
6528
6582
|
.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.
|
|
6583
|
+
.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
6584
|
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
6585
|
.nx-bubble.in { opacity:1; transform:none; }
|
|
6532
6586
|
.nx-bubble.out { opacity:0; transform:translateX(-24px); }
|
|
@@ -6535,7 +6589,7 @@
|
|
|
6535
6589
|
.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
6590
|
/* talk: speech bubbles */
|
|
6537
6591
|
.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.
|
|
6592
|
+
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
6593
|
.nx-bubble.nx-talk.in { transform:none; }
|
|
6540
6594
|
.nx-bubble.nx-talk.out { opacity:0; transform:translateY(-8px) scale(0.92); transition:opacity 0.3s ease, transform 0.3s ease; }
|
|
6541
6595
|
.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,8 +6605,30 @@
|
|
|
6551
6605
|
.nx-dots i:nth-child(2) { animation-delay:0.15s; } .nx-dots i:nth-child(3) { animation-delay:0.3s; }
|
|
6552
6606
|
@keyframes nxDot { 0%,60%,100% { transform:translateY(0); opacity:0.45; } 30% { transform:translateY(-4px); opacity:1; } }
|
|
6553
6607
|
@keyframes nxFadeIn { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:none; } }
|
|
6608
|
+
.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;
|
|
6609
|
+
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);
|
|
6610
|
+
opacity:0; transition:opacity 0.25s ease, transform 0.25s ease; pointer-events:auto; }
|
|
6611
|
+
.nx-mode-card.in { opacity:1; transform:translate(-50%, 0); }
|
|
6612
|
+
.nx-mode-head { display:flex; align-items:center; gap:7px; font-weight:800; font-size:14px; padding-right:26px; }
|
|
6613
|
+
.nx-mode-sub { margin-top:4px; font-size:12.5px; line-height:1.4; opacity:0.85; }
|
|
6614
|
+
.nx-mode-row { display:flex; gap:7px; margin-top:9px; flex-wrap:wrap; }
|
|
6615
|
+
.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; }
|
|
6616
|
+
.nx-mode-btn.primary { background:#69f0ae; border-color:#69f0ae; color:#061009; }
|
|
6617
|
+
.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; }
|
|
6618
|
+
.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;
|
|
6619
|
+
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));
|
|
6620
|
+
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); }
|
|
6621
|
+
.nx-next.in { opacity:1; transform:none; }
|
|
6622
|
+
.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; }
|
|
6623
|
+
.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; }
|
|
6624
|
+
.nx-next.counting .nx-next-ring { opacity:1; }
|
|
6625
|
+
.nx-next-ic { position:relative; }
|
|
6626
|
+
.nx-next-text { min-width:0; cursor:pointer; }
|
|
6627
|
+
.nx-next-k { font-size:11px; font-weight:800; opacity:0.75; text-transform:uppercase; letter-spacing:0.04em; }
|
|
6628
|
+
.nx-next-l { font-size:14px; font-weight:800; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
6629
|
+
.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; }
|
|
6554
6630
|
.nx-compose { position:absolute; left:50%; bottom:calc(112px + env(safe-area-inset-bottom, 0px)); z-index:2060; width:min(520px, calc(100% - 24px)); transform:translate(-50%, 16px) scale(0.96); opacity:0; pointer-events:none;
|
|
6555
|
-
background:rgba(16,18,24,0.
|
|
6631
|
+
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
6632
|
transition:opacity 0.22s ease, transform 0.25s cubic-bezier(.2,1.3,.4,1); display:flex; flex-direction:column; gap:6px; }
|
|
6557
6633
|
.nx-compose.open { opacity:1; transform:translate(-50%, 0) scale(1); pointer-events:auto; }
|
|
6558
6634
|
.nx-compose-head { display:flex; align-items:center; gap:6px; padding:0 2px 0 6px; }
|
|
@@ -6562,6 +6638,10 @@
|
|
|
6562
6638
|
.nx-compose-row { display:flex; gap:6px; }
|
|
6563
6639
|
.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
6640
|
.nx-compose-input:focus { border-color:var(--c, #69f0ae); }
|
|
6641
|
+
.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); }
|
|
6642
|
+
.nx-compose-x.nx-compose-full:hover { background:rgba(255,255,255,0.2); }
|
|
6643
|
+
.nx-hist-count:empty { display:none; }
|
|
6644
|
+
.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
6645
|
.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
6646
|
.nx-compose-send:active, .nx-compose-x:active { transform:scale(0.92); }
|
|
6567
6647
|
.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 +6684,8 @@
|
|
|
6604
6684
|
.nx-latency-label { font-weight:700; }
|
|
6605
6685
|
.nx-latency-range { flex:1 1 140px; accent-color:var(--c, #69f0ae); }
|
|
6606
6686
|
.nx-latency-live { flex-basis:100%; font-size:11px; opacity:0.65; }
|
|
6687
|
+
.nx-talk-pending { opacity:0.45; animation:nxPend 1s ease-in-out infinite alternate; }
|
|
6688
|
+
@keyframes nxPend { from { opacity:0.25; } to { opacity:0.6; } }
|
|
6607
6689
|
.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
6690
|
.nx-quick-field { flex:1 1 auto; min-width:120px; padding:6px 12px; font-size:14px; }
|
|
6609
6691
|
.quick-edit { opacity:0.75; }
|
|
@@ -6677,6 +6759,8 @@
|
|
|
6677
6759
|
.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
6760
|
.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
6761
|
.rx-mini { opacity:0; filter:drop-shadow(0 1px 2px rgba(0,0,0,0.5)); }
|
|
6762
|
+
.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; }
|
|
6763
|
+
.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
6764
|
.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
6765
|
.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
6766
|
.rx-glow { top:0 !important; bottom:0; width:min(90px, 14%); opacity:0; }
|
|
@@ -6705,8 +6789,9 @@
|
|
|
6705
6789
|
.cc-btn.cc-mode { gap:6px; padding:0 12px 0 10px; background:rgba(255,255,255,0.12); font-size:12px; }
|
|
6706
6790
|
.cc-btn.cc-mode:hover { background:rgba(255,255,255,0.22); }
|
|
6707
6791
|
.cc-mode-label { white-space:nowrap; }
|
|
6708
|
-
.cc-scrim { position:absolute; inset:0; z-index:59; pointer-events:none; background:
|
|
6709
|
-
|
|
6792
|
+
.cc-scrim { position:absolute; inset:0; z-index:59; pointer-events:none; background:transparent; transition:opacity 0.35s ease; }
|
|
6793
|
+
/* The picture stays fully clear while the controls show; the controls carry their own shadow. */
|
|
6794
|
+
.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
6795
|
.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
6796
|
.cc-top { pointer-events:none; }
|
|
6712
6797
|
.cc-top .cc-btn { pointer-events:auto; }
|
|
@@ -6800,7 +6885,7 @@
|
|
|
6800
6885
|
.nexus-container.cinema-on .sub-overlay-container { transition:bottom 0.35s ease, opacity 0.3s ease; }
|
|
6801
6886
|
.cinema-chrome.touch .cc-back, .cinema-chrome.touch .cc-fwd, .cinema-chrome.touch .cc-play { display:none; }
|
|
6802
6887
|
.cinema-chrome.touch .cc-seek { height:28px; }
|
|
6803
|
-
@container nexus (max-width: 560px) { .cc-
|
|
6888
|
+
@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
6889
|
@container nexus (max-width: 380px) { .cc-ambient { display:none; } }
|
|
6805
6890
|
@container nexus (max-width: 600px) {
|
|
6806
6891
|
.dock { padding:8px; gap:5px; }
|
|
@@ -6886,6 +6971,8 @@
|
|
|
6886
6971
|
const btnSyncStatus = mk('nxu-sync-status', '📡 ' + t('sync'));
|
|
6887
6972
|
const btnInvite = mk('nxu-invite', '👥 ' + t('invite'), t('inviteTitle'));
|
|
6888
6973
|
const btnCinema = mk('nxu-cinema', '🍿 ' + t('cinema'), t('cinemaTitle'));
|
|
6974
|
+
const btnAutoCinema = mk('nxu-auto-cinema', '');
|
|
6975
|
+
btnAutoCinema.classList.add('subtle');
|
|
6889
6976
|
const btnPiP = mk('nxu-pip', '📺 PiP');
|
|
6890
6977
|
const btnRate = mk('nxu-rate', '⏩ 1x', t('rateTitle'));
|
|
6891
6978
|
// Talking happens in speech bubbles and quick ink now: the menu opens those directly
|
|
@@ -6998,7 +7085,8 @@
|
|
|
6998
7085
|
const toast = createEl('div', 'toast');
|
|
6999
7086
|
toast.setAttribute('role', 'status');
|
|
7000
7087
|
|
|
7001
|
-
|
|
7088
|
+
// Sketching and chat live in the reaction bar (🖍️ 💬); the menu does not repeat them.
|
|
7089
|
+
dock.append(btnFindMedia, btnLink, btnInvite, btnSyncStatus, btnCinema, btnAutoCinema, btnPiP, btnRate, btnBookmark, btnTranslate, btnSubLog, btnDebug, btnHideSite);
|
|
7002
7090
|
uiWrapper.append(dock, reactPill, fab);
|
|
7003
7091
|
nexusContainer.append(drawCanvas, uiWrapper, chatPanel, subLogPanel, manualModal, transModal, toast, debugPanel);
|
|
7004
7092
|
shadow.appendChild(nexusContainer);
|
|
@@ -7009,7 +7097,7 @@
|
|
|
7009
7097
|
['keyup', 'keypress'].forEach(evt => nexusContainer.addEventListener(evt, e => { if (isEditableTarget(e.composedPath()[0])) e.stopPropagation(); }));
|
|
7010
7098
|
|
|
7011
7099
|
uiElements = {
|
|
7012
|
-
uiWrapper, fab, reactPill, dock, btnFindMedia, btnLink, btnSyncStatus, btnTranslate, btnCinema, btnPiP, btnRate, btnDraw, btnBookmark, btnChat, btnSubLog,
|
|
7100
|
+
uiWrapper, fab, reactPill, dock, btnFindMedia, btnLink, btnSyncStatus, btnTranslate, btnCinema, btnAutoCinema, btnPiP, btnRate, btnDraw, btnBookmark, btnChat, btnSubLog,
|
|
7013
7101
|
btnDebug, btnHideSite, chatPanel, chatMessages, chatInput, chatJumpBtn, subLogPanel, subLogMessages,
|
|
7014
7102
|
subLogJumpBtn, drawCanvas, manualModal, transModal, toast, debugJumpBtn, syncBubble: null,
|
|
7015
7103
|
pinError, guestInput, pinHint, pinDisplay, pinCopyBtn, hostBtn, connectBtn, btnInvite, typingRow, nickInput
|
|
@@ -7244,6 +7332,7 @@
|
|
|
7244
7332
|
};
|
|
7245
7333
|
|
|
7246
7334
|
btnCinema.onclick = () => { triggerHaptic(); CINEMA.toggle(); };
|
|
7335
|
+
btnAutoCinema.onclick = () => { triggerHaptic(); AUTO_CINEMA.setSite(!AUTO_CINEMA.enabled()); };
|
|
7247
7336
|
btnPiP.onclick = async () => {
|
|
7248
7337
|
let video = STATE.video;
|
|
7249
7338
|
if (!video || video.readyState === 0) {
|
|
@@ -7471,7 +7560,7 @@
|
|
|
7471
7560
|
},
|
|
7472
7561
|
publish() {
|
|
7473
7562
|
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');
|
|
7563
|
+
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
7564
|
} catch (_) { }
|
|
7476
7565
|
},
|
|
7477
7566
|
// Small editor: name + colour swatches + any colour.
|
|
@@ -7587,25 +7676,43 @@
|
|
|
7587
7676
|
});
|
|
7588
7677
|
},
|
|
7589
7678
|
// → { text, from } or null when no translation is needed / possible.
|
|
7679
|
+
_pending: new Map(),
|
|
7590
7680
|
async translate(text) {
|
|
7591
7681
|
const tl = myLanguage();
|
|
7592
7682
|
const key = tl + '\n' + text;
|
|
7593
7683
|
if (this._cache.has(key)) return this._cache.get(key);
|
|
7594
7684
|
if (!/[\p{L}]{2,}/u.test(text)) return null; // emoji, numbers, "ㅋ"
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7685
|
+
// The bubble and the room chat ask for the same message at the same moment.
|
|
7686
|
+
if (this._pending.has(key)) return this._pending.get(key);
|
|
7687
|
+
const job = this._fetch(text, tl).then((result) => {
|
|
7688
|
+
// Only real answers are remembered: a network hiccup (slow iPad link, a
|
|
7689
|
+
// blocked request) must not leave that message untranslated for good.
|
|
7690
|
+
if (result !== undefined) {
|
|
7691
|
+
this._cache.set(key, result);
|
|
7692
|
+
if (this._cache.size > 200) this._cache.delete(this._cache.keys().next().value);
|
|
7693
|
+
}
|
|
7694
|
+
return result || null;
|
|
7695
|
+
}).finally(() => this._pending.delete(key));
|
|
7696
|
+
this._pending.set(key, job);
|
|
7697
|
+
return job;
|
|
7698
|
+
},
|
|
7699
|
+
// → { text, from } | null (no translation needed) | undefined (failed: try again later)
|
|
7700
|
+
async _fetch(text, tl) {
|
|
7701
|
+
const g = code => (code === 'zh' ? 'zh-CN' : code);
|
|
7702
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${g(tl)}&dt=t&q=${encodeURIComponent(text.slice(0, 500))}`;
|
|
7703
|
+
// Through the userscript manager first, then directly (some iPad setups only allow one).
|
|
7704
|
+
for (const viaFetch of [false, true]) {
|
|
7705
|
+
try {
|
|
7706
|
+
const res = await _httpRequest({ url, timeout: 9000, viaFetch });
|
|
7707
|
+
if (res.status !== 200) continue;
|
|
7600
7708
|
const data = JSON.parse(res.text);
|
|
7601
7709
|
const out = Array.isArray(data?.[0]) ? data[0].map(x => (x && x[0]) || '').join('').trim() : '';
|
|
7602
7710
|
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;
|
|
7711
|
+
if (out && from && from !== baseLang(tl) && out.toLowerCase() !== text.trim().toLowerCase()) return { text: out, from };
|
|
7712
|
+
return null;
|
|
7713
|
+
} catch (_) { }
|
|
7714
|
+
}
|
|
7715
|
+
return undefined;
|
|
7609
7716
|
}
|
|
7610
7717
|
};
|
|
7611
7718
|
|
|
@@ -7635,17 +7742,21 @@
|
|
|
7635
7742
|
const reply = createEl('span', 'nx-talk-reply', '↩');
|
|
7636
7743
|
card.append(reply);
|
|
7637
7744
|
}
|
|
7638
|
-
|
|
7745
|
+
let timer = null, left = Math.max(4200, Math.min(11000, 3500 + text.length * 70)), startedAt = 0;
|
|
7746
|
+
if (!mine && CHAT_TR.enabled() && /[\p{L}]{2,}/u.test(text)) {
|
|
7747
|
+
const pend = createEl('span', 'nx-talk-orig nx-talk-pending', '🌐 …');
|
|
7748
|
+
body.querySelector('.nx-bubble-text').after(pend);
|
|
7639
7749
|
CHAT_TR.translate(text).then((tr) => {
|
|
7640
|
-
|
|
7750
|
+
pend.remove();
|
|
7751
|
+
if (!tr || !card.isConnected || card.classList.contains('out')) return;
|
|
7641
7752
|
const main = body.querySelector('.nx-bubble-text');
|
|
7642
7753
|
main.textContent = tr.text;
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
left
|
|
7754
|
+
main.after(createEl('span', 'nx-talk-orig', '🌐 ' + text));
|
|
7755
|
+
// Time to read the translation too (the bubble was already counting down).
|
|
7756
|
+
if (startedAt) { clearTimeout(timer); left = Math.max(left - (Date.now() - startedAt), 0) + 2500 + tr.text.length * 40; run(); }
|
|
7757
|
+
else left += 2500;
|
|
7646
7758
|
});
|
|
7647
7759
|
}
|
|
7648
|
-
let timer = null, left = Math.max(4200, Math.min(11000, 3500 + text.length * 70)), startedAt = 0;
|
|
7649
7760
|
const close = () => { clearTimeout(timer); card.classList.add('out'); setTimeout(() => card.remove(), 300); };
|
|
7650
7761
|
const run = () => { startedAt = Date.now(); timer = setTimeout(close, left); };
|
|
7651
7762
|
const hold = () => { clearTimeout(timer); left = Math.max(1500, left - (Date.now() - startedAt)); };
|
|
@@ -7687,7 +7798,8 @@
|
|
|
7687
7798
|
const to = createEl('span', 'nx-compose-to');
|
|
7688
7799
|
const x = createEl('button', 'nx-compose-x', '✕', { type: 'button', title: t('talkClose'), 'aria-label': t('talkClose') });
|
|
7689
7800
|
x.onclick = (e) => { e.stopPropagation(); this.close(); };
|
|
7690
|
-
const full = createEl('button', 'nx-compose-x nx-compose-full', '
|
|
7801
|
+
const full = createEl('button', 'nx-compose-x nx-compose-full', '', { type: 'button', title: t('talkFull'), 'aria-label': t('talkFull') });
|
|
7802
|
+
full.append(createEl('span', 'nx-hist-ic', '🗂'), createEl('span', 'nx-hist-label', t('talkHistory')), createEl('span', 'nx-hist-count'));
|
|
7691
7803
|
full.onclick = (e) => { e.stopPropagation(); this.close(); openChatPanel(); };
|
|
7692
7804
|
const me = createEl('button', 'nx-compose-x nx-me-dot', '', { type: 'button', title: t('profileTitle'), 'aria-label': t('profileTitle') });
|
|
7693
7805
|
me.style.setProperty('--c', STATE.color);
|
|
@@ -7721,6 +7833,10 @@
|
|
|
7721
7833
|
const to = this._composer.querySelector('.nx-compose-to');
|
|
7722
7834
|
to.textContent = replyTo ? t('talkReplyTo', { name: replyTo }) : '💬 ' + t('talkTitle');
|
|
7723
7835
|
this._composer.style.setProperty('--c', color || STATE.color || '#69f0ae');
|
|
7836
|
+
// How much history the room chat holds (shown on the "History" pill).
|
|
7837
|
+
const n = (() => { try { return yChat ? yChat.length : 0; } catch (_) { return 0; } })();
|
|
7838
|
+
const cnt = this._composer.querySelector('.nx-hist-count');
|
|
7839
|
+
if (cnt) cnt.textContent = n ? (n > 99 ? '99+' : String(n)) : '';
|
|
7724
7840
|
this._composer.classList.add('open');
|
|
7725
7841
|
this._fitKeyboard();
|
|
7726
7842
|
if (!this._vvBound && window.visualViewport) {
|
|
@@ -8035,6 +8151,253 @@
|
|
|
8035
8151
|
}
|
|
8036
8152
|
};
|
|
8037
8153
|
|
|
8154
|
+
// ───────────────────────── Automatic cinema
|
|
8155
|
+
// Opening a page that is all about one video (an episode, a movie — not a feed with many
|
|
8156
|
+
// posts) switches to cinema mode as soon as the video is ready. It keeps the site's own
|
|
8157
|
+
// player, because subtitles and servers are often picked there; a small card says which
|
|
8158
|
+
// player is in use and switches to NexusSync's player in one tap. Per site on/off.
|
|
8159
|
+
const AUTO_CINEMA = {
|
|
8160
|
+
_doneFor: '',
|
|
8161
|
+
_auto: false,
|
|
8162
|
+
_card: null,
|
|
8163
|
+
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,
|
|
8164
|
+
_sites() { const v = STORE.get('nxu_auto_cinema_sites', {}); return v && typeof v === 'object' ? v : {}; },
|
|
8165
|
+
enabled() {
|
|
8166
|
+
const v = this._sites()[HOST];
|
|
8167
|
+
if (v === true || v === false) return v;
|
|
8168
|
+
return !this.FEED_HOSTS.test(HOST);
|
|
8169
|
+
},
|
|
8170
|
+
setSite(on) {
|
|
8171
|
+
const all = { ...this._sites(), [HOST]: !!on };
|
|
8172
|
+
const keys = Object.keys(all);
|
|
8173
|
+
if (keys.length > 300) delete all[keys[0]];
|
|
8174
|
+
STORE.set('nxu_auto_cinema_sites', all);
|
|
8175
|
+
this.paintButton();
|
|
8176
|
+
showToast(t(on ? 'autoCinemaOnSite' : 'autoCinemaOffSite', { site: HOST }));
|
|
8177
|
+
},
|
|
8178
|
+
paintButton() {
|
|
8179
|
+
const b = uiElements.btnAutoCinema;
|
|
8180
|
+
if (!b) return;
|
|
8181
|
+
const on = this.enabled();
|
|
8182
|
+
b.textContent = '✨ ' + t(on ? 'autoCinemaBtnOn' : 'autoCinemaBtnOff');
|
|
8183
|
+
b.classList.toggle('active', on);
|
|
8184
|
+
},
|
|
8185
|
+
// One real video on the page, big enough, long enough — not a feed.
|
|
8186
|
+
_singleMedia() {
|
|
8187
|
+
if (/\/shorts\//.test(location.pathname) || /\/reel(s)?\//.test(location.pathname)) return false;
|
|
8188
|
+
if (document.querySelectorAll('article').length >= 4) return false;
|
|
8189
|
+
const doc = document.scrollingElement || document.documentElement;
|
|
8190
|
+
if (doc && doc.scrollHeight > window.innerHeight * 10) return false;
|
|
8191
|
+
let big = 0;
|
|
8192
|
+
for (const v of document.querySelectorAll('video')) {
|
|
8193
|
+
const r = v.getBoundingClientRect();
|
|
8194
|
+
if (r.width >= 200 && r.height >= 110) big++;
|
|
8195
|
+
}
|
|
8196
|
+
for (const [ifr] of VIDEO_DETECTOR.frameReports) {
|
|
8197
|
+
const r = ifr.getBoundingClientRect();
|
|
8198
|
+
if (r.width >= 200 && r.height >= 110) big++;
|
|
8199
|
+
}
|
|
8200
|
+
if (big > 1) return false;
|
|
8201
|
+
const info = currentMediaInfo();
|
|
8202
|
+
if (!info) return false;
|
|
8203
|
+
const r = info.el.getBoundingClientRect();
|
|
8204
|
+
if (r.width < Math.min(420, window.innerWidth * 0.6)) return false;
|
|
8205
|
+
return Number.isFinite(info.duration) && info.duration >= 90;
|
|
8206
|
+
},
|
|
8207
|
+
tick() {
|
|
8208
|
+
if (!IS_TOP || STATE.retired || STATE.isCinema || STATE.pendingCinema || document.hidden) return;
|
|
8209
|
+
const key = location.href.split('#')[0];
|
|
8210
|
+
if (this._doneFor === key || !this.enabled() || STORE.get('nxu_hide_fab_' + HOST, false)) return;
|
|
8211
|
+
if (!(STATE.video || STATE.cinemaIframe)) return;
|
|
8212
|
+
if (STATE.video && STATE.video.readyState < 1) return;
|
|
8213
|
+
if (!this._singleMedia()) return;
|
|
8214
|
+
this._doneFor = key;
|
|
8215
|
+
this._auto = true;
|
|
8216
|
+
CINEMA.setControls('site', { save: false });
|
|
8217
|
+
CINEMA.set(true, { silent: true });
|
|
8218
|
+
if (!STATE.isCinema) { this._auto = false; STATE.controlsOverride = null; }
|
|
8219
|
+
},
|
|
8220
|
+
onEnter() {
|
|
8221
|
+
const auto = this._auto;
|
|
8222
|
+
this._auto = false;
|
|
8223
|
+
setTimeout(() => this._showCard(auto), 400);
|
|
8224
|
+
},
|
|
8225
|
+
onExit() {
|
|
8226
|
+
this._doneFor = location.href.split('#')[0]; // left on purpose: not again on this page
|
|
8227
|
+
this._hideCard();
|
|
8228
|
+
},
|
|
8229
|
+
_hideCard() {
|
|
8230
|
+
const c = this._card;
|
|
8231
|
+
this._card = null;
|
|
8232
|
+
clearTimeout(this._cardTimer);
|
|
8233
|
+
if (c) { c.classList.remove('in'); setTimeout(() => c.remove(), 260); }
|
|
8234
|
+
},
|
|
8235
|
+
// Which player is in use, with a one-tap switch (and the per-site switch after an automatic start).
|
|
8236
|
+
_showCard(auto) {
|
|
8237
|
+
if (!STATE.isCinema || !IS_TOP) return;
|
|
8238
|
+
const container = getContainer();
|
|
8239
|
+
if (!container) return;
|
|
8240
|
+
this._hideCard();
|
|
8241
|
+
const site = CINEMA_UI.controlsMode() === 'site';
|
|
8242
|
+
const card = createEl('div', 'nx-mode-card');
|
|
8243
|
+
const head = createEl('div', 'nx-mode-head');
|
|
8244
|
+
head.append(createEl('span', 'nx-mode-ic', site ? '🎛️' : '🍿'), createEl('span', 'nx-mode-title', t(site ? 'modeSitePlayer' : 'modeNexusPlayer')));
|
|
8245
|
+
const sub = createEl('div', 'nx-mode-sub', t(site ? (auto ? 'modeSiteHintAuto' : 'modeSiteHint') : 'modeNexusHint'));
|
|
8246
|
+
const row = createEl('div', 'nx-mode-row');
|
|
8247
|
+
const sw = createEl('button', 'nx-mode-btn primary', t(site ? 'modeUseNexus' : 'modeUseSite'), { type: 'button' });
|
|
8248
|
+
sw.onclick = (e) => {
|
|
8249
|
+
e.stopPropagation();
|
|
8250
|
+
const next = CINEMA_UI.controlsMode() === 'site' ? 'nexus' : 'site';
|
|
8251
|
+
CINEMA_UI._resetGestures?.();
|
|
8252
|
+
CINEMA.setControls(next, { save: false });
|
|
8253
|
+
CINEMA_UI.sync();
|
|
8254
|
+
this._hideCard();
|
|
8255
|
+
setTimeout(() => this._showCard(false), 350);
|
|
8256
|
+
};
|
|
8257
|
+
row.appendChild(sw);
|
|
8258
|
+
if (auto) {
|
|
8259
|
+
const off = createEl('button', 'nx-mode-btn', t('autoCinemaStopSite'), { type: 'button' });
|
|
8260
|
+
off.onclick = (e) => { e.stopPropagation(); this.setSite(false); this._hideCard(); CINEMA.set(false, { silent: true }); };
|
|
8261
|
+
row.appendChild(off);
|
|
8262
|
+
}
|
|
8263
|
+
const x = createEl('button', 'nx-mode-x', '✕', { type: 'button', 'aria-label': t('talkClose') });
|
|
8264
|
+
x.onclick = (e) => { e.stopPropagation(); this._hideCard(); };
|
|
8265
|
+
card.append(x, head, sub, row);
|
|
8266
|
+
['pointerdown', 'click', 'touchstart'].forEach(ev => card.addEventListener(ev, (e) => e.stopPropagation()));
|
|
8267
|
+
container.appendChild(card);
|
|
8268
|
+
this._card = card;
|
|
8269
|
+
requestAnimationFrame(() => card.classList.add('in'));
|
|
8270
|
+
this._cardTimer = setTimeout(() => this._hideCard(), auto ? 9000 : 4500);
|
|
8271
|
+
resetIdleTimer(true);
|
|
8272
|
+
}
|
|
8273
|
+
};
|
|
8274
|
+
setInterval(() => { try { AUTO_CINEMA.tick(); } catch (_) { } }, 1200);
|
|
8275
|
+
|
|
8276
|
+
// ───────────────────────── Next episode
|
|
8277
|
+
// On series pages (drama/anime sites) the page usually has a "next episode" link or
|
|
8278
|
+
// button. Near the end of an episode in cinema mode a small card offers it, and when the
|
|
8279
|
+
// episode ends it counts down and moves on (in a room: everyone is invited along, and
|
|
8280
|
+
// this device comes back into the same room on the next page).
|
|
8281
|
+
const NEXT_EP = {
|
|
8282
|
+
_card: null,
|
|
8283
|
+
_target: null,
|
|
8284
|
+
_scanAt: 0,
|
|
8285
|
+
_cancelledFor: '',
|
|
8286
|
+
_count: null,
|
|
8287
|
+
enabled() { return STORE.get('nxu_auto_next', true) !== false; },
|
|
8288
|
+
RX_TEXT: /(다음\s*(화|회|편|회차|에피소드|episode|ep\.?)|다음\s*$|^\s*다음|next\s*(episode|ep\b|ep\.|part|chapter|video)|^\s*next\s*$|次の(話|エピソード)|次へ|下一[集话話]|siguiente\s*episodio|épisode\s*suivant|nächste\s*folge|próximo\s*episódio|⏭|»\s*$)/i,
|
|
8289
|
+
RX_ATTR: /(next[-_ ]?(ep|episode|video|btn|button|link)|btn[-_]?next|episode[-_]?next|nav[-_]?next|\bnext\b)/i,
|
|
8290
|
+
RX_BAD: /(prev|이전|previous|前|上一|comment|댓글|page|페이지|article|post|기사|뉴스)/i,
|
|
8291
|
+
_visible(el) {
|
|
8292
|
+
if (!el.isConnected || el.closest('#nxu-host')) return false;
|
|
8293
|
+
const r = el.getBoundingClientRect();
|
|
8294
|
+
if (r.width < 6 || r.height < 6) return false;
|
|
8295
|
+
const cs = getComputedStyle(el);
|
|
8296
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && +cs.opacity > 0.05;
|
|
8297
|
+
},
|
|
8298
|
+
_label(el) {
|
|
8299
|
+
return [el.textContent, el.getAttribute('aria-label'), el.getAttribute('title'), el.getAttribute('alt')].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
8300
|
+
},
|
|
8301
|
+
// → { el, href, label } or null
|
|
8302
|
+
find() {
|
|
8303
|
+
const here = location.origin + location.pathname + location.search;
|
|
8304
|
+
const out = [];
|
|
8305
|
+
const rel = document.querySelector('a[rel~="next"][href], link[rel~="next"][href]');
|
|
8306
|
+
if (rel) out.push({ el: rel.tagName === 'A' ? rel : null, href: rel.href, label: rel.tagName === 'A' ? this._label(rel) : '', score: 40 });
|
|
8307
|
+
const nodes = document.querySelectorAll('a[href], button, [role="button"], [onclick]');
|
|
8308
|
+
let n = 0;
|
|
8309
|
+
for (const el of nodes) {
|
|
8310
|
+
if (++n > 4000) break;
|
|
8311
|
+
const label = this._label(el);
|
|
8312
|
+
const attrs = `${el.id} ${typeof el.className === 'string' ? el.className : ''} ${el.getAttribute('data-action') || ''}`;
|
|
8313
|
+
let score = 0;
|
|
8314
|
+
if (this.RX_TEXT.test(label)) score += 60;
|
|
8315
|
+
if (this.RX_ATTR.test(attrs)) score += 25;
|
|
8316
|
+
if (!score) continue;
|
|
8317
|
+
if (this.RX_BAD.test(label) || /prev/i.test(attrs)) score -= 70;
|
|
8318
|
+
if (score <= 0 || !this._visible(el)) continue;
|
|
8319
|
+
const href = el.tagName === 'A' ? el.href : '';
|
|
8320
|
+
if (href) {
|
|
8321
|
+
if (/^javascript:/i.test(el.getAttribute('href') || '') || href.split('#')[0] === here) { if (!el.onclick && !el.hasAttribute('onclick')) continue; }
|
|
8322
|
+
else if (new URL(href).origin === location.origin) score += 10;
|
|
8323
|
+
}
|
|
8324
|
+
if (label.length > 60) score -= 15;
|
|
8325
|
+
out.push({ el, href, label, score });
|
|
8326
|
+
}
|
|
8327
|
+
out.sort((a, b) => b.score - a.score);
|
|
8328
|
+
const best = out[0];
|
|
8329
|
+
return best && best.score >= 40 ? best : null;
|
|
8330
|
+
},
|
|
8331
|
+
go(target = this._target) {
|
|
8332
|
+
if (!target) return;
|
|
8333
|
+
this.hide();
|
|
8334
|
+
showToast('⏭ ' + t('nextEpGoing'));
|
|
8335
|
+
const href = target.href && !/^javascript:/i.test(target.href) ? target.href : '';
|
|
8336
|
+
if (href && href.split('#')[0] !== location.href.split('#')[0]) {
|
|
8337
|
+
if (SOCIAL.live()) { TRAVEL.go(href); return; }
|
|
8338
|
+
location.href = href;
|
|
8339
|
+
return;
|
|
8340
|
+
}
|
|
8341
|
+
// A button that changes the episode with script: remember the room, then press it.
|
|
8342
|
+
try { TRAVEL.onLeavePage(); } catch (_) { }
|
|
8343
|
+
try { target.el?.click(); } catch (_) { }
|
|
8344
|
+
},
|
|
8345
|
+
hide() {
|
|
8346
|
+
clearInterval(this._count);
|
|
8347
|
+
this._count = null;
|
|
8348
|
+
const c = this._card;
|
|
8349
|
+
this._card = null;
|
|
8350
|
+
if (c) { c.classList.remove('in'); setTimeout(() => c.remove(), 260); }
|
|
8351
|
+
},
|
|
8352
|
+
_show(target, ended) {
|
|
8353
|
+
const container = getContainer();
|
|
8354
|
+
if (!container) return;
|
|
8355
|
+
if (!this._card || !this._card.isConnected) {
|
|
8356
|
+
const card = createEl('div', 'nx-next');
|
|
8357
|
+
const play = createEl('button', 'nx-next-go', '', { type: 'button' });
|
|
8358
|
+
play.append(createEl('span', 'nx-next-ring'), createEl('span', 'nx-next-ic', '⏭'));
|
|
8359
|
+
const text = createEl('div', 'nx-next-text');
|
|
8360
|
+
text.append(createEl('div', 'nx-next-k', t('nextEpTitle')), createEl('div', 'nx-next-l'));
|
|
8361
|
+
const cancel = createEl('button', 'nx-next-x', '✕', { type: 'button', title: t('nextEpCancel'), 'aria-label': t('nextEpCancel') });
|
|
8362
|
+
card.append(play, text, cancel);
|
|
8363
|
+
['pointerdown', 'click', 'touchstart'].forEach(ev => card.addEventListener(ev, (e) => e.stopPropagation()));
|
|
8364
|
+
play.onclick = () => this.go();
|
|
8365
|
+
text.onclick = () => this.go();
|
|
8366
|
+
cancel.onclick = () => { this._cancelledFor = location.href; this.hide(); UI_GUARD.shield(); };
|
|
8367
|
+
container.appendChild(card);
|
|
8368
|
+
this._card = card;
|
|
8369
|
+
requestAnimationFrame(() => card.classList.add('in'));
|
|
8370
|
+
}
|
|
8371
|
+
this._card.querySelector('.nx-next-l').textContent = target.label && target.label.length > 2 ? target.label : t('nextEpGo');
|
|
8372
|
+
if (ended && !this._count && this.enabled()) {
|
|
8373
|
+
let left = 8;
|
|
8374
|
+
const ring = this._card.querySelector('.nx-next-ring');
|
|
8375
|
+
this._card.classList.add('counting');
|
|
8376
|
+
ring.style.setProperty('--p', '0');
|
|
8377
|
+
ring.dataset.n = String(left);
|
|
8378
|
+
this._count = setInterval(() => {
|
|
8379
|
+
left--;
|
|
8380
|
+
ring.dataset.n = String(Math.max(0, left));
|
|
8381
|
+
ring.style.setProperty('--p', String((8 - left) / 8));
|
|
8382
|
+
if (left <= 0) { clearInterval(this._count); this._count = null; this.go(); }
|
|
8383
|
+
}, 1000);
|
|
8384
|
+
}
|
|
8385
|
+
},
|
|
8386
|
+
tick() {
|
|
8387
|
+
if (!IS_TOP || !STATE.isCinema) { if (this._card) this.hide(); return; }
|
|
8388
|
+
const info = currentMediaInfo();
|
|
8389
|
+
if (!info || !Number.isFinite(info.duration) || info.duration < 240) { if (this._card) this.hide(); return; }
|
|
8390
|
+
const left = info.duration - info.time;
|
|
8391
|
+
const ended = left <= 0.8 || (info.kind === 'video' && info.el.ended);
|
|
8392
|
+
if (left > 25 && !ended) { if (this._card) this.hide(); this._target = null; return; }
|
|
8393
|
+
if (this._cancelledFor === location.href) return;
|
|
8394
|
+
if (!this._target || Date.now() - this._scanAt > 5000) { this._scanAt = Date.now(); this._target = this.find(); }
|
|
8395
|
+
if (!this._target) return;
|
|
8396
|
+
this._show(this._target, ended);
|
|
8397
|
+
}
|
|
8398
|
+
};
|
|
8399
|
+
setInterval(() => { try { NEXT_EP.tick(); } catch (_) { } }, 1000);
|
|
8400
|
+
|
|
8038
8401
|
// ───────────────────────── Cover: hide the picture for a moment (for everyone)
|
|
8039
8402
|
// 🙈 (or H in cinema, Alt+Shift+H anywhere) lays a soft, nearly opaque curtain over the
|
|
8040
8403
|
// picture on every device in the room — for a violent or scary scene when children are
|
|
@@ -8679,7 +9042,7 @@
|
|
|
8679
9042
|
// Animations run on the compositor (Web Animations), with a hard cap on live nodes.
|
|
8680
9043
|
// Button feedback in the reaction bar: a springy pop, a ring and a few tiny copies of the
|
|
8681
9044
|
// emoji bursting out around the button (kept inside the bar's neighbourhood).
|
|
8682
|
-
const PRESS_COLORS = { '
|
|
9045
|
+
const PRESS_COLORS = { '😂': '#ffd54f', '😭': '#82b1ff', '😡': '#ff5252', '💦': '#4fc3f7', '🍪': '#d7a86e', '☕': '#bcaaa4', '😘': '#ff6fae', '🖕': '#ffb74d' };
|
|
8683
9046
|
function pressFx(btn, emoji) {
|
|
8684
9047
|
if (!btn) return;
|
|
8685
9048
|
btn.style.setProperty('--pop', PRESS_COLORS[emoji] || '#69f0ae');
|
|
@@ -8715,9 +9078,10 @@
|
|
|
8715
9078
|
_combo: new Map(), // lane → { n, timer, badge }
|
|
8716
9079
|
_tags: new Map(), // uid → last name-tag time
|
|
8717
9080
|
reduced() { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (_) { return false; } },
|
|
8718
|
-
_node(cls, text, x, y, size) {
|
|
9081
|
+
_node(cls, text, x, y, size, force = false) {
|
|
8719
9082
|
const container = getContainer();
|
|
8720
|
-
|
|
9083
|
+
// The burst's ring and glow always get a place, even when many small bits are flying.
|
|
9084
|
+
if (!container || (this.live >= this.MAX && !force)) return null;
|
|
8721
9085
|
const el = createEl('div', 'rx ' + cls, text);
|
|
8722
9086
|
el.style.left = x + 'px';
|
|
8723
9087
|
el.style.top = y + 'px';
|
|
@@ -8772,7 +9136,7 @@
|
|
|
8772
9136
|
const sway = (Math.random() < 0.5 ? -1 : 1) * (10 + Math.random() * 14);
|
|
8773
9137
|
const d = 1700 + Math.random() * 500;
|
|
8774
9138
|
const base = { duration: d, delay };
|
|
8775
|
-
const kind = { '❤️': 'heart', '💗': 'heart', '😂': 'laugh', '😭': 'cry', '😡': 'angry', '🫣': 'peek', '🌰': 'toss', '🚽': 'flush' }[e] || 'float';
|
|
9139
|
+
const kind = { '❤️': 'heart', '💗': 'heart', '😂': 'laugh', '😭': 'cry', '😡': 'angry', '💦': 'splash', '🍪': 'munch', '☕': 'steam', '😘': 'kiss', '🖕': 'flip', '🫣': 'peek', '🌰': 'toss', '🚽': 'flush' }[e] || 'float';
|
|
8776
9140
|
if (this.reduced()) { this._run(el, [{ opacity: 0 }, { opacity: 1, offset: 0.2 }, { opacity: 0, transform: `translateY(${-R * 0.4}px)` }], base); return; }
|
|
8777
9141
|
switch (kind) {
|
|
8778
9142
|
case 'heart':
|
|
@@ -8822,6 +9186,101 @@
|
|
|
8822
9186
|
{ const puff = this._node('rx-mini', '💢', x + size * 0.7, lane.y - R * 0.3, 16);
|
|
8823
9187
|
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
9188
|
break;
|
|
9189
|
+
case 'splash': {
|
|
9190
|
+
// Shoots up with a little shiver, then bursts into droplets.
|
|
9191
|
+
this._run(el, [
|
|
9192
|
+
{ transform: 'translate(0,0) scale(0.3) rotate(0)', opacity: 0 },
|
|
9193
|
+
{ transform: `translate(0, ${-R * 0.42}px) scale(1.25) rotate(-6deg)`, opacity: 1, offset: 0.3 },
|
|
9194
|
+
{ transform: `translate(3px, ${-R * 0.46}px) scale(1.15) rotate(6deg)`, offset: 0.38 },
|
|
9195
|
+
{ transform: `translate(-3px, ${-R * 0.5}px) scale(1.2) rotate(-4deg)`, offset: 0.46 },
|
|
9196
|
+
{ transform: `translate(0, ${-R * 0.55}px) scale(1.5)`, opacity: 0, offset: 0.6 },
|
|
9197
|
+
{ transform: `translate(0, ${-R * 0.55}px) scale(1.5)`, opacity: 0 }
|
|
9198
|
+
], { ...base, duration: 1500 });
|
|
9199
|
+
for (let i = 0; i < 6; i++) {
|
|
9200
|
+
const ang = -Math.PI / 2 + (i - 2.5) * 0.42;
|
|
9201
|
+
const dist = 40 + Math.random() * 34;
|
|
9202
|
+
const drop = this._node('rx-mini', '💧', x + size * 0.35, lane.y - R * 0.55, 13 + Math.random() * 5);
|
|
9203
|
+
this._run(drop, [
|
|
9204
|
+
{ transform: 'translate(0,0) scale(0.4)', opacity: 0 },
|
|
9205
|
+
{ transform: `translate(${Math.cos(ang) * dist}px, ${Math.sin(ang) * dist}px) scale(1)`, opacity: 1, offset: 0.4 },
|
|
9206
|
+
{ transform: `translate(${Math.cos(ang) * dist * 1.3}px, ${Math.sin(ang) * dist + 70}px) scale(0.7)`, opacity: 0 }
|
|
9207
|
+
], { duration: 900, delay: delay + 850, easing: 'cubic-bezier(.2,.6,.6,1)' });
|
|
9208
|
+
}
|
|
9209
|
+
break;
|
|
9210
|
+
}
|
|
9211
|
+
case 'munch': {
|
|
9212
|
+
// Rises, gets two quick bites (squash), crumbs fall.
|
|
9213
|
+
this._run(el, [
|
|
9214
|
+
{ transform: 'translate(0,0) scale(0.4) rotate(-20deg)', opacity: 0 },
|
|
9215
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.4}px) scale(1.15) rotate(8deg)`, opacity: 1, offset: 0.28 },
|
|
9216
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.44}px) scale(1.25, 0.85) rotate(0)`, offset: 0.36 },
|
|
9217
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.47}px) scale(1.1) rotate(-4deg)`, offset: 0.44 },
|
|
9218
|
+
{ transform: `translate(${sway * 0.4}px, ${-R * 0.5}px) scale(1.25, 0.85)`, offset: 0.52 },
|
|
9219
|
+
{ transform: `translate(${-sway * 0.3}px, ${-R * 0.85}px) scale(0.9) rotate(10deg)`, opacity: 0 }
|
|
9220
|
+
], base);
|
|
9221
|
+
for (let i = 0; i < 5; i++) {
|
|
9222
|
+
const crumb = this._node('rx-crumb', '', x + size * (0.25 + Math.random() * 0.5), lane.y - R * 0.45, 0);
|
|
9223
|
+
this._run(crumb, [
|
|
9224
|
+
{ transform: 'translate(0,0) scale(0.6)', opacity: 0 },
|
|
9225
|
+
{ transform: `translate(${(Math.random() - 0.5) * 30}px, 8px) scale(1)`, opacity: 1, offset: 0.2 },
|
|
9226
|
+
{ transform: `translate(${(Math.random() - 0.5) * 50}px, ${50 + Math.random() * 40}px) rotate(${Math.random() * 180}deg)`, opacity: 0 }
|
|
9227
|
+
], { duration: 800, delay: delay + d * 0.34 + i * 60, easing: 'ease-in' });
|
|
9228
|
+
}
|
|
9229
|
+
break;
|
|
9230
|
+
}
|
|
9231
|
+
case 'steam': {
|
|
9232
|
+
// A calm cup: slow rise, gentle sway, steam curling up.
|
|
9233
|
+
this._run(el, [
|
|
9234
|
+
{ transform: 'translate(0,0) scale(0.6)', opacity: 0 },
|
|
9235
|
+
{ transform: `translate(${sway * 0.3}px, ${-R * 0.25}px) scale(1.05)`, opacity: 1, offset: 0.25 },
|
|
9236
|
+
{ transform: `translate(${-sway * 0.3}px, ${-R * 0.45}px) scale(1.05)`, offset: 0.55 },
|
|
9237
|
+
{ transform: `translate(${sway * 0.2}px, ${-R * 0.6}px) scale(0.95)`, opacity: 0 }
|
|
9238
|
+
], { ...base, duration: 2400, easing: 'ease-in-out' });
|
|
9239
|
+
for (let i = 0; i < 3; i++) {
|
|
9240
|
+
const wisp = this._node('rx-steam', '', x + size * (0.3 + i * 0.18), lane.y - R * 0.3 - size * 0.6, 0);
|
|
9241
|
+
this._run(wisp, [
|
|
9242
|
+
{ transform: 'translate(0,0) scale(0.6, 0.8)', opacity: 0 },
|
|
9243
|
+
{ transform: `translate(${i % 2 ? 6 : -6}px, -22px) scale(1, 1.2)`, opacity: 0.8, offset: 0.4 },
|
|
9244
|
+
{ transform: `translate(${i % 2 ? -4 : 4}px, -52px) scale(1.4, 1.6)`, opacity: 0 }
|
|
9245
|
+
], { duration: 1500, delay: delay + 500 + i * 220, easing: 'ease-out' });
|
|
9246
|
+
}
|
|
9247
|
+
break;
|
|
9248
|
+
}
|
|
9249
|
+
case 'kiss': {
|
|
9250
|
+
// Leans in, then blows a kiss that floats toward the middle of the screen.
|
|
9251
|
+
const toward = lane.x > lane.W / 2 ? -1 : 1;
|
|
9252
|
+
this._run(el, [
|
|
9253
|
+
{ transform: 'translate(0,0) scale(0.4) rotate(0)', opacity: 0 },
|
|
9254
|
+
{ transform: `translate(0, ${-R * 0.35}px) scale(1.2) rotate(${toward * 12}deg)`, opacity: 1, offset: 0.3 },
|
|
9255
|
+
{ transform: `translate(${toward * 6}px, ${-R * 0.38}px) scale(1.05, 1.15) rotate(${toward * 18}deg)`, offset: 0.45 },
|
|
9256
|
+
{ transform: `translate(0, ${-R * 0.55}px) scale(0.9) rotate(0)`, opacity: 0 }
|
|
9257
|
+
], base);
|
|
9258
|
+
for (let i = 0; i < 2; i++) {
|
|
9259
|
+
const heart = this._node('rx-mini', i ? '💗' : '💋', x + size * 0.5, lane.y - R * 0.38, i ? 15 : 18);
|
|
9260
|
+
this._run(heart, [
|
|
9261
|
+
{ transform: 'translate(0,0) scale(0.3)', opacity: 0 },
|
|
9262
|
+
{ transform: `translate(${toward * 40}px, -16px) scale(1.15)`, opacity: 1, offset: 0.3 },
|
|
9263
|
+
{ transform: `translate(${toward * 110}px, -40px) scale(1)`, offset: 0.7 },
|
|
9264
|
+
{ transform: `translate(${toward * 150}px, -60px) scale(0.6)`, opacity: 0 }
|
|
9265
|
+
], { duration: 1300, delay: delay + d * 0.42 + i * 180, easing: 'ease-out' });
|
|
9266
|
+
}
|
|
9267
|
+
break;
|
|
9268
|
+
}
|
|
9269
|
+
case 'flip':
|
|
9270
|
+
// Cheeky: pops up, two jabs toward the screen, a shake, gone.
|
|
9271
|
+
this._run(el, [
|
|
9272
|
+
{ transform: 'translate(0,0) scale(0.3)', opacity: 0 },
|
|
9273
|
+
{ transform: `translate(0, ${-R * 0.3}px) scale(1.1)`, opacity: 1, offset: 0.18 },
|
|
9274
|
+
{ transform: `translate(0, ${-R * 0.33}px) scale(1.45)`, offset: 0.26 },
|
|
9275
|
+
{ transform: `translate(0, ${-R * 0.34}px) scale(1.1)`, offset: 0.33 },
|
|
9276
|
+
{ transform: `translate(0, ${-R * 0.36}px) scale(1.45)`, offset: 0.41 },
|
|
9277
|
+
{ transform: `translate(-4px, ${-R * 0.38}px) scale(1.15) rotate(-8deg)`, offset: 0.5 },
|
|
9278
|
+
{ transform: `translate(4px, ${-R * 0.4}px) scale(1.15) rotate(8deg)`, offset: 0.57 },
|
|
9279
|
+
{ transform: `translate(0, ${-R * 0.7}px) scale(0.9)`, opacity: 0 }
|
|
9280
|
+
], { ...base, easing: 'ease-out' });
|
|
9281
|
+
{ const pop = this._node('rx-mini', '💥', x + size * 0.6, lane.y - R * 0.36, 16);
|
|
9282
|
+
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 }); }
|
|
9283
|
+
break;
|
|
8825
9284
|
case 'peek':
|
|
8826
9285
|
this._run(el, [
|
|
8827
9286
|
{ transform: `translate(${lane.x > lane.W / 2 ? 60 : -60}px, ${-R * 0.3}px) scale(1)`, opacity: 0 },
|
|
@@ -8883,12 +9342,12 @@
|
|
|
8883
9342
|
_burst(e, side, color) {
|
|
8884
9343
|
const lane = this._lane(side);
|
|
8885
9344
|
const ox = side === 'right' ? lane.W - 30 : 30, oy = lane.y + 20;
|
|
8886
|
-
const ring = this._node('rx-ring', '', ox, oy);
|
|
9345
|
+
const ring = this._node('rx-ring', '', ox, oy, 0, true);
|
|
8887
9346
|
if (ring) {
|
|
8888
9347
|
ring.style.setProperty('--c', color);
|
|
8889
9348
|
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
9349
|
}
|
|
8891
|
-
const glow = this._node('rx-glow ' + side, '', 0, 0);
|
|
9350
|
+
const glow = this._node('rx-glow ' + side, '', 0, 0, 0, true);
|
|
8892
9351
|
if (glow) {
|
|
8893
9352
|
glow.style.setProperty('--c', color);
|
|
8894
9353
|
this._run(glow, [{ opacity: 0 }, { opacity: 1, offset: 0.25 }, { opacity: 0 }], { duration: 1100 });
|
|
@@ -9148,7 +9607,7 @@
|
|
|
9148
9607
|
_scrubbing: false,
|
|
9149
9608
|
_wasPaused: null,
|
|
9150
9609
|
|
|
9151
|
-
controlsMode() { return
|
|
9610
|
+
controlsMode() { return cinemaControlsMode(); },
|
|
9152
9611
|
|
|
9153
9612
|
_setIcon(el, name, size = 22, label = '') {
|
|
9154
9613
|
const key = `${name}|${size}|${label}`;
|
|
@@ -9225,7 +9684,7 @@
|
|
|
9225
9684
|
const subBtn = btn('cc-sub', 'cc', t('aiSubTitle'));
|
|
9226
9685
|
const fitBtn = btn('cc-fit', 'fit', t('ccFit'));
|
|
9227
9686
|
const ambientBtn = btn('cc-ambient', 'ambient', t('ccAmbient'));
|
|
9228
|
-
const pipBtn = btn('cc-pip', 'pip', '
|
|
9687
|
+
const pipBtn = btn('cc-pip', 'pip', t('ccPip'));
|
|
9229
9688
|
const fsBtn = btn('cc-fs', 'fullscreen', t('ccFullscreen'));
|
|
9230
9689
|
row.append(playBtn, backBtn, fwdBtn, vol, time, live, spacer, rateBtn, subBtn, fitBtn, ambientBtn, pipBtn, fsBtn);
|
|
9231
9690
|
bottom.append(seek, row);
|
|
@@ -10537,6 +10996,7 @@
|
|
|
10537
10996
|
if (uiElements.btnRate) uiElements.btnRate.style.display = hasMedia ? '' : 'none';
|
|
10538
10997
|
updateRateButton();
|
|
10539
10998
|
if (uiElements.btnCinema) uiElements.btnCinema.style.display = hasMedia || STATE.isCinema ? '' : 'none';
|
|
10999
|
+
if (uiElements.btnAutoCinema) { uiElements.btnAutoCinema.style.display = hasMedia || STATE.isCinema ? '' : 'none'; AUTO_CINEMA.paintButton(); }
|
|
10540
11000
|
if (uiElements.btnBookmark) uiElements.btnBookmark.style.display = hasMedia ? '' : 'none';
|
|
10541
11001
|
if (uiElements.btnFindMedia) uiElements.btnFindMedia.style.display = hasMedia ? 'none' : '';
|
|
10542
11002
|
if (uiElements.btnLink) uiElements.btnLink.style.display = hasMedia || connected ? '' : 'none';
|
|
@@ -10587,7 +11047,7 @@
|
|
|
10587
11047
|
initFirebaseStreaming(STATE.handshakeVerified ? 'nexus_rooms' : (NETWORK.streamNode || 'nexus_rooms'),
|
|
10588
11048
|
STATE.handshakeVerified ? STATE.topicId : (NETWORK.streamNode === 'nexus_lobbies' ? STATE.lobbyId : STATE.topicId));
|
|
10589
11049
|
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 (_) { }
|
|
11050
|
+
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
11051
|
sendFirebaseCmd('heartbeat', { ts: Date.now() }, 'nexus_rooms');
|
|
10592
11052
|
syncServerClock();
|
|
10593
11053
|
if (RTC.isHost && STATE.connectionMode === 'HTTP_RELAY') RTC.init();
|
|
@@ -10596,6 +11056,8 @@
|
|
|
10596
11056
|
window.addEventListener('pageshow', (e) => { if (e.persisted) { DEVICE_GUARD.onVisible(60000); reviveConnection('page restore'); } });
|
|
10597
11057
|
window.addEventListener('pagehide', () => DEVICE_GUARD.onHidden());
|
|
10598
11058
|
document.addEventListener('visibilitychange', () => {
|
|
11059
|
+
// Tell the room at once whether this device is in the foreground (leadership).
|
|
11060
|
+
if (IS_TOP) try { PROFILE.publish(); } catch (_) { }
|
|
10599
11061
|
if (document.hidden) { _hiddenSince = Date.now(); DEVICE_GUARD.onHidden(); return; }
|
|
10600
11062
|
const away = _hiddenSince ? Date.now() - _hiddenSince : 0;
|
|
10601
11063
|
_hiddenSince = 0;
|
package/package.json
CHANGED