pinokiod 3.180.0 → 3.181.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.
@@ -1462,10 +1462,56 @@ if (typeof hotkeys === 'function') {
1462
1462
  })
1463
1463
  }
1464
1464
 
1465
+ // Stable per-browser device identifier
1466
+ (function initPinokioDeviceId() {
1467
+ if (typeof window === 'undefined') {
1468
+ return;
1469
+ }
1470
+ try {
1471
+ const KEY = 'pinokio:device-id';
1472
+ const gen = () => `${Date.now()}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
1473
+ const get = () => {
1474
+ try {
1475
+ let id = localStorage.getItem(KEY);
1476
+ if (typeof id !== 'string' || id.length < 8) {
1477
+ id = gen();
1478
+ localStorage.setItem(KEY, id);
1479
+ }
1480
+ return id;
1481
+ } catch (_) {
1482
+ // Fallback when localStorage is unavailable
1483
+ if (!window.__pinokioVolatileDeviceId) {
1484
+ window.__pinokioVolatileDeviceId = gen();
1485
+ }
1486
+ return window.__pinokioVolatileDeviceId;
1487
+ }
1488
+ };
1489
+ // Expose helpers
1490
+ if (!window.PinokioGetDeviceId) {
1491
+ window.PinokioGetDeviceId = get;
1492
+ }
1493
+ // Convenience alias
1494
+ window.PinokioDeviceId = get();
1495
+ } catch (_) {
1496
+ // ignore
1497
+ }
1498
+ })();
1499
+
1465
1500
  (function initNotificationAudioBridge() {
1466
1501
  if (typeof window === 'undefined') {
1467
1502
  return;
1468
1503
  }
1504
+ // Avoid duplicate audio playback: if this is the top-level layout page, or if the
1505
+ // top window already owns notification playback, skip initialising this bridge.
1506
+ try {
1507
+ const isTop = window.top === window;
1508
+ if (isTop && document.getElementById('layout-root')) {
1509
+ return; // layout shell handles notifications
1510
+ }
1511
+ if (!isTop && window.top && window.top.__pinokioTopNotifyListener) {
1512
+ return; // top-level listener active; avoid duplicates from iframes
1513
+ }
1514
+ } catch (_) {}
1469
1515
  if (window.__pinokioNotificationAudioInitialized) {
1470
1516
  return;
1471
1517
  }
@@ -1478,6 +1524,64 @@ if (typeof hotkeys === 'function') {
1478
1524
  let reconnectTimeout = null;
1479
1525
  let activeAudio = null;
1480
1526
 
1527
+ // Lightweight visual indicator to confirm notification receipt (mobile-friendly)
1528
+ let notifyIndicatorEl = null;
1529
+ let notifyIndicatorStyleInjected = false;
1530
+ const ensureNotifyIndicator = () => {
1531
+ if (!notifyIndicatorStyleInjected) {
1532
+ try {
1533
+ const style = document.createElement('style');
1534
+ style.textContent = `
1535
+ .pinokio-notify-indicator{position:fixed;top:12px;right:12px;z-index:2147483647;display:none;align-items:center;gap:8px;padding:8px 10px;border-radius:999px;background:rgba(15,23,42,0.92);color:#fff;font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 10px 30px rgba(0,0,0,0.35)}
1536
+ .pinokio-notify-indicator .bell{font-size:14px}
1537
+ .pinokio-notify-indicator.show{display:inline-flex;animation:pinokioNotifyPop 160ms ease-out, pinokioNotifyFade 1600ms ease-in 700ms forwards}
1538
+ @keyframes pinokioNotifyPop{from{transform:translateY(-6px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
1539
+ @keyframes pinokioNotifyFade{to{opacity:0;transform:translateY(-4px)}}
1540
+ @media (max-width: 768px){.pinokio-notify-indicator{top:10px;right:10px;padding:7px 9px;font-size:12px}}
1541
+ `;
1542
+ document.head.appendChild(style);
1543
+ notifyIndicatorStyleInjected = true;
1544
+ } catch (_) {}
1545
+ }
1546
+ if (!notifyIndicatorEl) {
1547
+ try {
1548
+ const el = document.createElement('div');
1549
+ el.className = 'pinokio-notify-indicator';
1550
+ const icon = document.createElement('span');
1551
+ icon.className = 'bell';
1552
+ icon.textContent = '🔔';
1553
+ const text = document.createElement('span');
1554
+ text.className = 'text';
1555
+ text.textContent = 'Notification received';
1556
+ el.appendChild(icon);
1557
+ el.appendChild(text);
1558
+ document.body.appendChild(el);
1559
+ notifyIndicatorEl = el;
1560
+ } catch (_) {}
1561
+ }
1562
+ };
1563
+ const flashNotifyIndicator = (payload) => {
1564
+ try {
1565
+ ensureNotifyIndicator();
1566
+ if (!notifyIndicatorEl) return;
1567
+ const text = notifyIndicatorEl.querySelector('.text');
1568
+ if (text) {
1569
+ const msg = (payload && typeof payload.message === 'string' && payload.message.trim()) ? payload.message.trim() : 'Notification received';
1570
+ // Keep it short on mobile
1571
+ text.textContent = msg.length > 80 ? (msg.slice(0, 77) + '…') : msg;
1572
+ }
1573
+ // retrigger animation
1574
+ notifyIndicatorEl.classList.remove('show');
1575
+ // force reflow
1576
+ void notifyIndicatorEl.offsetWidth;
1577
+ notifyIndicatorEl.classList.add('show');
1578
+ // Auto-hide handled by CSS animation; keep element for reuse
1579
+ window.setTimeout(() => {
1580
+ if (notifyIndicatorEl) notifyIndicatorEl.classList.remove('show');
1581
+ }, 2600);
1582
+ } catch (_) {}
1583
+ };
1584
+
1481
1585
  const leaderStorageKey = 'pinokio.notification.leader';
1482
1586
  const leaderHeartbeatMs = 5000;
1483
1587
  const leaderStaleMs = 15000;
@@ -1629,6 +1733,18 @@ if (typeof hotkeys === 'function') {
1629
1733
  return;
1630
1734
  }
1631
1735
  const payload = packet.data || {};
1736
+ // If targeted to a specific device, ignore only when our id exists and mismatches
1737
+ try {
1738
+ const targetId = (typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null;
1739
+ if (targetId) {
1740
+ const myId = (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : null;
1741
+ if (myId && myId !== targetId) {
1742
+ return;
1743
+ }
1744
+ }
1745
+ } catch (_) {}
1746
+ // Visual confirmation regardless of audio outcome (useful on mobile)
1747
+ flashNotifyIndicator(payload);
1632
1748
  if (typeof payload.sound === 'string' && payload.sound) {
1633
1749
  enqueueSound(payload.sound);
1634
1750
  }
@@ -1696,6 +1812,7 @@ if (typeof hotkeys === 'function') {
1696
1812
  {
1697
1813
  method: CHANNEL_ID,
1698
1814
  mode: 'listen',
1815
+ device_id: (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : undefined,
1699
1816
  },
1700
1817
  handlePacket
1701
1818
  );
@@ -1768,6 +1885,127 @@ if (typeof hotkeys === 'function') {
1768
1885
  // Attempt to become leader immediately on load.
1769
1886
  attemptLeadership();
1770
1887
  })();
1888
+
1889
+ // Mobile "Tap to connect" curtain to prime audio on the top-level page
1890
+ (function initMobileConnectCurtain() {
1891
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
1892
+ return;
1893
+ }
1894
+ try {
1895
+ if (window.__pinokioConnectCurtainInstalled || window.__pinokioConnectCurtainInstalling) {
1896
+ return;
1897
+ }
1898
+ } catch (_) {}
1899
+ try {
1900
+ if (window.top && window.top !== window) {
1901
+ return; // only top-level
1902
+ }
1903
+ } catch (_) {
1904
+ // cross-origin parent; just bail
1905
+ return;
1906
+ }
1907
+ if (window.__pinokioConnectCurtainInstalled) {
1908
+ return;
1909
+ }
1910
+
1911
+ const isLikelyMobile = () => {
1912
+ try {
1913
+ if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
1914
+ if (navigator.userAgentData.mobile) return true;
1915
+ }
1916
+ } catch (_) {}
1917
+ try {
1918
+ const ua = (navigator.userAgent || '').toLowerCase();
1919
+ if (/iphone|ipad|ipod|android|mobile/.test(ua)) return true;
1920
+ } catch (_) {}
1921
+ try {
1922
+ if (navigator.maxTouchPoints && navigator.maxTouchPoints > 1) return true;
1923
+ } catch (_) {}
1924
+ try {
1925
+ if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) return true;
1926
+ } catch (_) {}
1927
+ try {
1928
+ if (window.matchMedia && window.matchMedia('(max-width: 900px)').matches) return true;
1929
+ } catch (_) {}
1930
+ return false;
1931
+ };
1932
+
1933
+ const createCurtain = () => {
1934
+ const style = document.createElement('style');
1935
+ style.textContent = `
1936
+ .pinokio-connect-curtain{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;background:rgba(15,23,42,0.35);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center}
1937
+ .pinokio-connect-msg{user-select:none;-webkit-user-select:none;color:#fff;background:rgba(15,23,42,0.85);padding:14px 18px;border-radius:12px;font:600 16px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 16px 40px rgba(0,0,0,.38)}
1938
+ @media (max-width:768px){.pinokio-connect-msg{font-size:15px;padding:12px 16px}}
1939
+ `;
1940
+ document.head.appendChild(style);
1941
+
1942
+ const overlay = document.createElement('div');
1943
+ overlay.className = 'pinokio-connect-curtain';
1944
+ overlay.setAttribute('role', 'button');
1945
+ overlay.setAttribute('aria-label', 'Tap to connect');
1946
+ overlay.tabIndex = 0;
1947
+ const msg = document.createElement('div');
1948
+ msg.className = 'pinokio-connect-msg';
1949
+ msg.textContent = 'Tap to connect';
1950
+ overlay.appendChild(msg);
1951
+ window.__pinokioConnectCurtainInstalled = true;
1952
+ return overlay;
1953
+ };
1954
+
1955
+ const primeAudio = async () => {
1956
+ try {
1957
+ let a = window.__pinokioChimeAudio;
1958
+ if (!a) {
1959
+ a = new Audio('/chime.mp3');
1960
+ a.preload = 'auto';
1961
+ a.loop = false;
1962
+ a.muted = false;
1963
+ window.__pinokioChimeAudio = a;
1964
+ }
1965
+ a.currentTime = 0;
1966
+ await a.play(); // must be called synchronously in gesture handler
1967
+ try { a.pause(); a.currentTime = 0; } catch (_) {}
1968
+ try { window.__pinokioAudioArmed = true; } catch (_) {}
1969
+ return true;
1970
+ } catch (_) {
1971
+ try { window.__pinokioAudioArmed = true; } catch (_) {}
1972
+ return false;
1973
+ }
1974
+ };
1975
+
1976
+ const setup = () => {
1977
+ let forceParam = false;
1978
+ try {
1979
+ const usp = new URLSearchParams(window.location.search);
1980
+ forceParam = usp.has('connect') || usp.get('connect') === '1';
1981
+ } catch (_) {}
1982
+ if (!(forceParam || isLikelyMobile())) {
1983
+ return;
1984
+ }
1985
+ if (window.__pinokioConnectCurtainInstalled || window.__pinokioConnectCurtainInstalling) {
1986
+ return;
1987
+ }
1988
+ try { window.__pinokioConnectCurtainInstalling = true; } catch (_) {}
1989
+ const overlay = createCurtain();
1990
+ let handled = false;
1991
+ const onTap = async (e) => {
1992
+ if (handled) return;
1993
+ handled = true;
1994
+ try { e.preventDefault(); e.stopPropagation(); } catch (_) {}
1995
+ try { await primeAudio(); } catch (_) {}
1996
+ try { overlay.remove(); } catch (_) {}
1997
+ try { window.__pinokioConnectCurtainInstalled = true; window.__pinokioConnectCurtainInstalling = false; } catch (_) {}
1998
+ };
1999
+ overlay.addEventListener('pointerdown', onTap, { once: true, capture: true });
2000
+ document.body.appendChild(overlay);
2001
+ };
2002
+
2003
+ if (document.readyState === 'loading') {
2004
+ document.addEventListener('DOMContentLoaded', setup, { once: true });
2005
+ } else {
2006
+ setup();
2007
+ }
2008
+ })();
1771
2009
  const refreshParent = (e) => {
1772
2010
  let dispatched = false;
1773
2011
  if (typeof window !== 'undefined' && typeof window.PinokioBroadcastMessage === 'function') {
@@ -1967,6 +2205,12 @@ document.addEventListener("DOMContentLoaded", () => {
1967
2205
  }
1968
2206
  if (document.querySelector("#refresh-page")) {
1969
2207
  document.querySelector("#refresh-page").addEventListener("click", (e) => {
2208
+ try {
2209
+ const headerEl = document.querySelector("header.navheader");
2210
+ const isMinimized = !!(headerEl && headerEl.classList.contains("minimized"));
2211
+ const key = `pinokio:header-restore-once:${location.pathname}`;
2212
+ sessionStorage.setItem(key, isMinimized ? "1" : "0");
2213
+ } catch (_) {}
1970
2214
  location.reload()
1971
2215
  /*
1972
2216
  let browserview = document.querySelector(".browserview")
@@ -790,4 +790,118 @@
790
790
  };
791
791
 
792
792
  window.PinokioLayout = api;
793
+ // Mobile "Tap to connect" curtain is centralized in common.js to avoid duplicates
794
+
795
+ // Top-level notification listener (indicator + optional chime) for mobile
796
+ (function initTopLevelNotificationListener() {
797
+ try { if (window.top && window.top !== window) return; } catch (_) { return; }
798
+ if (window.__pinokioTopNotifyListener) {
799
+ return;
800
+ }
801
+ window.__pinokioTopNotifyListener = true;
802
+
803
+ const ensureIndicator = (() => {
804
+ let el = null;
805
+ let styleInjected = false;
806
+ return () => {
807
+ if (!styleInjected) {
808
+ const style = document.createElement('style');
809
+ style.textContent = `
810
+ .pinokio-notify-indicator{position:fixed;top:12px;right:12px;z-index:2147483647;display:none;align-items:center;gap:8px;padding:8px 10px;border-radius:999px;background:rgba(15,23,42,0.92);color:#fff;font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;box-shadow:0 10px 30px rgba(0,0,0,0.35)}
811
+ .pinokio-notify-indicator .bell{font-size:14px}
812
+ .pinokio-notify-indicator.show{display:inline-flex;animation:pinokioNotifyPop 160ms ease-out, pinokioNotifyFade 1600ms ease-in 700ms forwards}
813
+ @keyframes pinokioNotifyPop{from{transform:translateY(-6px) scale(.98);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}
814
+ @keyframes pinokioNotifyFade{to{opacity:0;transform:translateY(-4px)}}
815
+ @media (max-width: 768px){.pinokio-notify-indicator{top:10px;right:10px;padding:7px 9px;font-size:12px}}
816
+ `;
817
+ document.head.appendChild(style);
818
+ styleInjected = true;
819
+ }
820
+ if (!el) {
821
+ el = document.createElement('div');
822
+ el.className = 'pinokio-notify-indicator';
823
+ const icon = document.createElement('span');
824
+ icon.className = 'bell';
825
+ icon.textContent = '🔔';
826
+ const text = document.createElement('span');
827
+ text.className = 'text';
828
+ text.textContent = 'Notification received';
829
+ el.appendChild(icon);
830
+ el.appendChild(text);
831
+ document.body.appendChild(el);
832
+ }
833
+ return el;
834
+ };
835
+ })();
836
+
837
+ const flashIndicator = (message) => {
838
+ const node = ensureIndicator();
839
+ const text = node.querySelector('.text');
840
+ if (text) {
841
+ const msg = (message && typeof message === 'string' && message.trim()) ? message.trim() : 'Notification received';
842
+ text.textContent = msg.length > 80 ? (msg.slice(0,77) + '…') : msg;
843
+ }
844
+ node.classList.remove('show');
845
+ void node.offsetWidth;
846
+ node.classList.add('show');
847
+ setTimeout(() => node.classList.remove('show'), 2400);
848
+ };
849
+
850
+ const tryPlay = (url) => {
851
+ try {
852
+ const src = (typeof url === 'string' && url) ? url : '/chime.mp3';
853
+ let a = window.__pinokioChimeAudio;
854
+ if (!a) {
855
+ a = new Audio(src);
856
+ a.preload = 'auto';
857
+ a.loop = false;
858
+ a.muted = false;
859
+ window.__pinokioChimeAudio = a;
860
+ } else {
861
+ try { if (a.src && !a.src.endsWith(src)) a.src = src; } catch (_) {}
862
+ }
863
+ try { a.currentTime = 0; } catch (_) {}
864
+ const p = a.play();
865
+ if (p && typeof p.catch === 'function') { p.catch(() => {}); }
866
+ if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
867
+ try { navigator.vibrate(80); } catch (_) {}
868
+ }
869
+ } catch (_) {}
870
+ };
871
+
872
+ const listen = () => {
873
+ const SocketCtor = typeof window.Socket === 'function' ? window.Socket : (typeof Socket === 'function' ? Socket : null);
874
+ if (!SocketCtor || typeof WebSocket === 'undefined') {
875
+ return;
876
+ }
877
+ const socket = new SocketCtor();
878
+ try {
879
+ socket.run({ method: 'kernel.notifications', mode: 'listen', device_id: (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : undefined }, (packet) => {
880
+ if (!packet || packet.id !== 'kernel.notifications' || packet.type !== 'notification') {
881
+ return;
882
+ }
883
+ const payload = packet.data || {};
884
+ // If targeted to a specific device, ignore only when our id exists and mismatches
885
+ try {
886
+ const targetId = (typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null;
887
+ if (targetId) {
888
+ const myId = (typeof window.PinokioGetDeviceId === 'function') ? window.PinokioGetDeviceId() : null;
889
+ if (myId && myId !== targetId) return;
890
+ }
891
+ } catch (_) {}
892
+ flashIndicator(payload.message);
893
+ tryPlay(payload.sound);
894
+ }).then(() => {
895
+ // socket closed; ignore
896
+ }).catch(() => {});
897
+ window.__pinokioTopNotifySocket = socket;
898
+ } catch (_) {}
899
+ };
900
+
901
+ if (document.readyState === 'loading') {
902
+ document.addEventListener('DOMContentLoaded', listen, { once: true });
903
+ } else {
904
+ listen();
905
+ }
906
+ })();
793
907
  })();