Guest User

Untitled

a guest
Sep 6th, 2026
9
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 25.65 KB | None | 0 0
  1. CSS
  2.  
  3.  
  4. /* ==========================================================================
  5. 1. PINNED PANEL TRANSLUCENCY (HOVER & FOCUS DIFFERENTIATED)
  6. ========================================================================== */
  7. #zen-app-panel-root #zen-app-panel-slider {
  8. transition: opacity 0.22s ease, backdrop-filter 0.22s ease, background-color 0.22s ease !important;
  9. }
  10.  
  11. /* Default unpinned panel */
  12. #zen-app-panel-root:not([data-pinned="true"]) #zen-app-panel-slider {
  13. background-color: color-mix(in srgb, var(--tabpanels-background-color, #1e1e24) 92%, transparent) !important;
  14. backdrop-filter: blur(20px) saturate(140%) !important;
  15. opacity: 1 !important;
  16. }
  17.  
  18. /* Pinned + Hovered or Focused: readable translucent */
  19. #zen-app-panel-root[data-pinned="true"]:is(:hover, :focus-within) #zen-app-panel-slider {
  20. background-color: color-mix(in srgb, var(--tabpanels-background-color, #1e1e24) 65%, transparent) !important;
  21. backdrop-filter: blur(16px) saturate(130%) !important;
  22. opacity: 0.85 !important;
  23. }
  24.  
  25. /* Pinned + Unfocused: high transparency */
  26. #zen-app-panel-root[data-pinned="true"]:not(:hover):not(:focus-within) #zen-app-panel-slider {
  27. background-color: color-mix(in srgb, var(--tabpanels-background-color, #1e1e24) 35%, transparent) !important;
  28. backdrop-filter: blur(10px) saturate(120%) !important;
  29. opacity: 0.45 !important;
  30. }
  31.  
  32. /* Ensure pill menu is always 100% visible and interactive */
  33. #zen-app-panel-root #zen-app-panel-pill {
  34. opacity: 1 !important;
  35. pointer-events: auto !important;
  36. }
  37.  
  38. /* ==========================================================================
  39. 2. OPPOSITE-SIDE DOCKING, PILL MENU & RESIZE STRIP
  40. ========================================================================== */
  41. #browser,
  42. #tabbrowser-tabpanels,
  43. #tabbrowser-tabbox,
  44. .browserSidebarContainer,
  45. :root #sidebar-box,
  46. :root #navigator-toolbox {
  47. max-width: unset !important;
  48. }
  49.  
  50. /* Sidebar on Left -> Panel on Right */
  51. #zen-app-panel-root[data-panel-side="right"] {
  52. right: 12px !important;
  53. left: auto !important;
  54. }
  55. #zen-app-panel-root[data-panel-side="right"] .zen-app-resize-strip {
  56. left: -8px !important;
  57. right: auto !important;
  58. width: 14px !important;
  59. cursor: ew-resize !important;
  60. z-index: 30 !important;
  61. pointer-events: auto !important;
  62. }
  63. #zen-app-panel-root[data-panel-side="right"] #zen-app-panel-pill {
  64. left: 0px !important;
  65. right: auto !important;
  66. top: 50% !important;
  67. transform: translate(-100%, -50%) !important;
  68. pointer-events: auto !important;
  69. z-index: 40 !important;
  70. }
  71. #zen-app-panel-root[data-panel-side="right"] .zen-app-hover-zone {
  72. left: -24px !important;
  73. right: auto !important;
  74. pointer-events: auto !important;
  75. }
  76.  
  77. /* Sidebar on Right -> Panel on Left */
  78. #zen-app-panel-root[data-panel-side="left"] {
  79. left: 12px !important;
  80. right: auto !important;
  81. }
  82. #zen-app-panel-root[data-panel-side="left"] .zen-app-resize-strip {
  83. right: -8px !important;
  84. left: auto !important;
  85. width: 14px !important;
  86. cursor: ew-resize !important;
  87. z-index: 30 !important;
  88. pointer-events: auto !important;
  89. }
  90. #zen-app-panel-root[data-panel-side="left"] #zen-app-panel-pill {
  91. right: 0px !important;
  92. left: auto !important;
  93. top: 50% !important;
  94. transform: translate(100%, -50%) !important;
  95. pointer-events: auto !important;
  96. z-index: 40 !important;
  97. }
  98. #zen-app-panel-root[data-panel-side="left"] .zen-app-hover-zone {
  99. right: -24px !important;
  100. left: auto !important;
  101. pointer-events: auto !important;
  102. }
  103.  
  104. /* ==========================================================================
  105. 3. ISOLATE TABS & INDEPENDENT TILE STATES
  106. ========================================================================== */
  107. /* Reset tab-level filter and opacity so enclosed child tiles are never dimmed by tab state */
  108. .tabbrowser-tab:has(.zen-app-tile),
  109. .tabbrowser-tab[pending]:has(.zen-app-tile),
  110. .tabbrowser-tab[pending="true"]:has(.zen-app-tile),
  111. .tabbrowser-tab[zen-dormant="true"]:has(.zen-app-tile),
  112. .tabbrowser-tab[zen-essential="true"]:has(.zen-app-tile) {
  113. filter: none !important;
  114. opacity: 1 !important;
  115. }
  116.  
  117. /* Shift pending/dormant dimming strictly to the tab's internal icon and stack */
  118. .tabbrowser-tab[pending]:has(.zen-app-tile) > .tab-stack,
  119. .tabbrowser-tab[pending="true"]:has(.zen-app-tile) > .tab-stack,
  120. .tabbrowser-tab[zen-dormant="true"]:has(.zen-app-tile) > .tab-stack,
  121. .tabbrowser-tab[pending]:has(.zen-app-tile) .tab-icon-image,
  122. .tabbrowser-tab[pending="true"]:has(.zen-app-tile) .tab-icon-image,
  123. .tabbrowser-tab[zen-dormant="true"]:has(.zen-app-tile) .tab-icon-image {
  124. filter: grayscale(100%) !important;
  125. opacity: 0.45 !important;
  126. }
  127.  
  128. /* Selected/active tab restores full icon vibrancy */
  129. .tabbrowser-tab[selected="true"]:has(.zen-app-tile) .tab-icon-image,
  130. .tabbrowser-tab[selected="true"]:has(.zen-app-tile) .tab-stack {
  131. filter: none !important;
  132. opacity: 1 !important;
  133. }
  134.  
  135. /* The tile state is 100% independent */
  136. .zen-app-tile {
  137. filter: none !important;
  138. opacity: 1 !important;
  139. }
  140.  
  141. /* Unloaded / Dormant app panel: grayed out and semi-transparent */
  142. .zen-app-tile[data-unloaded="true"]:not([data-active="true"]),
  143. .zen-app-tile:not([data-loaded="true"]):not([data-active="true"]) {
  144. filter: grayscale(100%) !important;
  145. opacity: 0.35 !important;
  146. transition: opacity 0.2s ease, filter 0.2s ease !important;
  147. }
  148.  
  149. .zen-app-tile[data-unloaded="true"]:not([data-active="true"]):hover,
  150. .zen-app-tile:not([data-loaded="true"]):not([data-active="true"]):hover {
  151. opacity: 0.85 !important;
  152. filter: grayscale(20%) !important;
  153. }
  154.  
  155. /* Loaded or active app panel: FULL COLOR and 100% opacity regardless of tab state */
  156. .zen-app-tile[data-loaded="true"],
  157. .zen-app-tile[data-active="true"] {
  158. filter: none !important;
  159. opacity: 1 !important;
  160. }
  161.  
  162. /* ==========================================================================
  163. 4. ENCLOSED CORNER-DOCKED BUTTONS (FLUSH BOTTOM-RIGHT EDGES)
  164. ========================================================================== */
  165. .tabbrowser-tab[pinned="true"],
  166. .tabbrowser-tab[zen-essential="true"],
  167. #zen-essentials-container .tabbrowser-tab {
  168. position: relative !important;
  169. }
  170.  
  171. .tabbrowser-tab[pinned="true"] .zen-app-tile,
  172. .tabbrowser-tab[zen-essential="true"] .zen-app-tile,
  173. #zen-essentials-container .tabbrowser-tab .zen-app-tile,
  174. #zen-apps-sidebar-grid .zen-app-tile {
  175. position: absolute !important;
  176. right: 0px !important;
  177. bottom: 0px !important;
  178. width: 22px !important;
  179. height: 22px !important;
  180. min-width: 22px !important;
  181. min-height: 22px !important;
  182. max-width: 22px !important;
  183. max-height: 22px !important;
  184. padding: 0 !important;
  185. margin: 0 !important;
  186. display: flex !important;
  187. align-items: center !important;
  188. justify-content: center !important;
  189. border-radius: 6px 0 4px 0 !important;
  190. pointer-events: all !important;
  191. cursor: pointer !important;
  192. box-shadow: -1px -1px 3px rgba(0, 0, 0, 0.5) !important;
  193. background: rgba(22, 22, 28, 0.94) !important;
  194. border-top: 1px solid rgba(255, 255, 255, 0.15) !important;
  195. border-left: 1px solid rgba(255, 255, 255, 0.15) !important;
  196. border-right: none !important;
  197. border-bottom: none !important;
  198. transform: none !important;
  199. z-index: 99999 !important;
  200. }
  201.  
  202. .zen-app-tile:hover {
  203. background: rgba(45, 45, 55, 0.98) !important;
  204. border-top-color: rgba(255, 255, 255, 0.35) !important;
  205. border-left-color: rgba(255, 255, 255, 0.35) !important;
  206. }
  207.  
  208. /* 14px icon sizing */
  209. .zen-app-tile img,
  210. .zen-app-tile svg,
  211. .zen-app-tile image {
  212. width: 14px !important;
  213. height: 14px !important;
  214. min-width: 14px !important;
  215. min-height: 14px !important;
  216. max-width: 14px !important;
  217. max-height: 14px !important;
  218. pointer-events: none !important;
  219. }
  220.  
  221. /* Notification badge */
  222. .zen-app-tile .zen-app-badge {
  223. top: -3px !important;
  224. right: -3px !important;
  225. min-width: 10px !important;
  226. height: 10px !important;
  227. font-size: 7px !important;
  228. line-height: 10px !important;
  229. padding: 0 2px !important;
  230. pointer-events: none !important;
  231. filter: none !important;
  232. opacity: 1 !important;
  233. }
  234.  
  235. /* Hide fallback autohide elements */
  236. #zen-apps-sidebar-grid .zen-apps-autohide-dots,
  237. #zen-apps-sidebar-grid #zentral-apps-utility-section {
  238. display: none !important;
  239. }
  240.  
  241. /* Ensure the pill menu always wins the stacking fight against corner-docked tiles */
  242. #zen-app-panel-root #zen-app-panel-pill {
  243. z-index: 999999 !important; /* was implicitly lower than .zen-app-tile's 99999 */
  244. }
  245.  
  246. /* Make the resize strip's hit target forgiving even at very small panel widths */
  247. #zen-app-panel-root .zen-app-resize-strip {
  248. min-width: 14px !important;
  249. }
  250. /* Hide Expand/Restore: its width math only works for same-side docking, not our
  251. opposite-side setup, so it shrinks the panel instead of growing it. */
  252. #zen-app-panel-pill .zen-app-btn[title="Expand panel"],
  253. #zen-app-panel-pill .zen-app-btn[title="Restore panel"] {
  254. display: none !important;
  255. }
  256.  
  257.  
  258.  
  259.  
  260.  
  261.  
  262.  
  263.  
  264.  
  265.  
  266.  
  267.  
  268.  
  269.  
  270.  
  271.  
  272.  
  273.  
  274.  
  275.  
  276.  
  277.  
  278.  
  279. JS
  280.  
  281. /* ==========================================================================
  282. ZENTRAL EXTENSION: CORNER DOCK ON ESSENTIAL TABS, LOADED STATES,
  283. OPPOSITE-SIDE DOCKING, RELIABLE MIDDLE-CLICK UNLOAD
  284. (v3 - fixes the expand-width math for real, event-driven docking instead
  285. of polling, and proper cleanup so hot-reloads don't stack listeners)
  286. ========================================================================== */
  287. (() => {
  288. const patchZentralApps = () => {
  289. const apps = window.Zentral?.Apps || window.ZenApps || window.zentral?.apps;
  290. if (!apps) {
  291. setTimeout(patchZentralApps, 150);
  292. return;
  293. }
  294.  
  295. // Idempotency guard: `window` persists across a Sine hot-reload of this
  296. // script, so without this a reload would double-wrap every method and
  297. // double-register every document-level listener below. The flag is
  298. // cleared again in the unload handler so a genuine reload still works.
  299. if (window.__zentralOppositeDockPatched) return;
  300. window.__zentralOppositeDockPatched = true;
  301.  
  302. const proto = Object.getPrototypeOf(apps);
  303. const cleanupFns = [];
  304. const registerCleanup = (fn) => cleanupFns.push(fn);
  305.  
  306. // Wraps/replaces a method on both the instance and its prototype, and
  307. // remembers how to put the original back on unload.
  308. const patchMethod = (name, makeWrapper) => {
  309. const orig = proto?.[name] || apps[name];
  310. const wrapped = makeWrapper(orig);
  311. apps[name] = wrapped;
  312. if (proto && proto !== Object.prototype) proto[name] = wrapped;
  313. registerCleanup(() => {
  314. apps[name] = orig;
  315. if (proto && proto !== Object.prototype) proto[name] = orig;
  316. });
  317. return orig;
  318. };
  319.  
  320. const isSidebarOnRight = () => {
  321. try {
  322. if (typeof apps.isSidebarRight === "function") return apps.isSidebarRight();
  323. if (typeof proto?.isSidebarRight === "function") return proto.isSidebarRight.call(apps);
  324. } catch (_) {}
  325. return document.documentElement.getAttribute("zen-right-side") === "true" ||
  326. document.documentElement.getAttribute("zen-sidebar-right") === "true";
  327. };
  328.  
  329. // 1. PREVENT RELOCATING TO TOP URL TOOLBAR
  330. patchMethod("isPhysicallySidebarCollapsed", () => () => false);
  331.  
  332. // 2. INVERT DOCKING: OPPOSITE OF VERTICAL TAB BAR (sidebar placement only —
  333. // vertical-bar placement keeps its own original left/right logic untouched)
  334. patchMethod("isPanelAttachedToRight", (orig) => function () {
  335. if (typeof this.isPlacementVerticalBar === "function" && this.isPlacementVerticalBar()) {
  336. return typeof orig === "function" ? orig.call(this) : !isSidebarOnRight();
  337. }
  338. return !isSidebarOnRight();
  339. });
  340.  
  341. // 3. Let the base mod's OWN positionPanel do all the real work — it already
  342. // reads isPanelAttachedToRight() above and correctly computes the
  343. // sidebar-width-aware, collapsed-aware left/right/top/bottom. We just
  344. // piggyback afterward to keep our custom pin-state attribute in sync.
  345. patchMethod("positionPanel", (orig) => function () {
  346. if (typeof orig === "function") {
  347. try { orig.call(this); } catch (_) {}
  348. }
  349. syncPinState();
  350. attachPillListeners();
  351. });
  352.  
  353. // 4. FIX THE EXPAND-WIDTH MATH FOR OPPOSITE-SIDE DOCKING
  354. // The panel's left/right *position* already looks correct because our
  355. // CSS pins it with `!important` (right:12px / left:12px based on
  356. // data-panel-side) regardless of what JS computes. Width has no such
  357. // safety net, and the base toggleExpand() assumes the panel docks on
  358. // the SAME side as the real sidebar — with our inversion that produces
  359. // a negative/garbage width (clamped down to the ~280px minimum), which
  360. // is exactly the "expand shrinks the panel" bug. We let the original
  361. // run first (it correctly toggles state, updates the button icon, and
  362. // handles the vertical-bar placement mode, which isn't affected by our
  363. // inversion), then — only in the "just expanded" + sidebar-placement
  364. // case — recompute the correct width ourselves and overwrite it via
  365. // the public updateWidthVar(), using the same 12px gap our CSS uses so
  366. // the panel's open edge lines up exactly with where CSS pins the fixed
  367. // edge.
  368. patchMethod("toggleExpand", (orig) => function () {
  369. if (typeof orig === "function") {
  370. try { orig.call(this); } catch (_) {}
  371. }
  372. if (typeof this.isPlacementVerticalBar === "function" && this.isPlacementVerticalBar()) return;
  373.  
  374. const pill = document.getElementById("zen-app-panel-pill");
  375. const expandBtn = pill?.querySelector(
  376. ".zen-app-btn[title='Restore panel'], .zen-app-btn[title='Expand panel']"
  377. );
  378. const justExpanded = expandBtn?.title === "Restore panel";
  379. if (!justExpanded) return; // restoring uses a cached width, which is side-independent and already correct
  380.  
  381. const gap = 12;
  382. const sidebarEl = document.getElementById("sidebar-box") ||
  383. document.getElementById("sidebar-container") ||
  384. document.getElementById("vertical-tabs");
  385. const sRect = sidebarEl?.getBoundingClientRect();
  386. if (!sRect || sRect.width === 0) return; // no measurable sidebar; leave whatever the base computed
  387.  
  388. let fullWidth = this.isPanelAttachedToRight()
  389. ? (window.innerWidth - gap) - (sRect.right + gap) // panel on right, sidebar on left: fill the gap between them
  390. : (sRect.left - gap) - gap; // panel on left, sidebar on right: fill the gap between them
  391.  
  392. fullWidth = Math.max(280, Math.min(fullWidth, window.innerWidth - 100));
  393. if (typeof this.updateWidthVar === "function") this.updateWidthVar(fullWidth);
  394. });
  395.  
  396. const syncPinState = () => {
  397. const root = document.getElementById("zen-app-panel-root");
  398. if (!root) return;
  399. const pinBtn = document.querySelector(
  400. "#zen-app-panel-pill .zen-app-btn[title*='Pin'], #zen-app-panel-pill .zen-app-btn[title*='pin']"
  401. );
  402. const isPinned = pinBtn?.getAttribute("data-pinned") === "true";
  403. root.setAttribute("data-pinned", isPinned ? "true" : "false");
  404. };
  405.  
  406. // We don't intercept any pill button clicks (expand/pin/refresh/close/grab
  407. // all already work correctly on their own) — just piggyback to resync our
  408. // custom pin-state attribute a moment after any interaction with the pill.
  409. const attachPillListeners = () => {
  410. const pill = document.getElementById("zen-app-panel-pill");
  411. if (!pill || pill._listenersAttached) return;
  412. pill._listenersAttached = true;
  413. const handler = () => setTimeout(syncPinState, 50);
  414. pill.addEventListener("click", handler);
  415. registerCleanup(() => pill.removeEventListener("click", handler));
  416. };
  417.  
  418. // === LEFT-CLICK: don't let it also select the essential tab underneath ===
  419. const tileMousedownGuard = (e) => {
  420. if (e.button !== 0) return;
  421. if (e.target.closest?.(".zen-app-tile[data-app-id]")) e.stopPropagation();
  422. };
  423. document.addEventListener("mousedown", tileMousedownGuard, { capture: true });
  424. registerCleanup(() => document.removeEventListener("mousedown", tileMousedownGuard, { capture: true }));
  425.  
  426. // === RELIABLE MIDDLE-CLICK UNLOAD ===============================================
  427. // The base tile already fully implements left-click (toggle panel) and
  428. // right-click (context menu) — we must not add handlers for those or we'd
  429. // double-fire them. The only real gap is middle click: the base tile
  430. // ignores every button except 0, so it falls through to the tab
  431. // underneath. Zen/Firefox's "middle-click closes tab" is wired at the
  432. // chrome level via a plain "click" event (not just "auxclick"), so we
  433. // intercept all three at capture phase on `document` — the earliest
  434. // possible point — to reliably win the race regardless of event type or
  435. // where the tab's own handler is attached.
  436. const unloadApp = (appId) => {
  437. if (!appId) return;
  438. if (typeof apps.closeApp === "function") {
  439. try { apps.closeApp(appId); } catch (_) {}
  440. }
  441. document.querySelectorAll(`.zen-app-tile[data-app-id="${appId}"] .zen-app-badge`)
  442. .forEach((b) => b.remove());
  443. };
  444.  
  445. const middleClickHandlers = {};
  446. ["mousedown", "click", "auxclick"].forEach((type) => {
  447. const handler = (e) => {
  448. if (e.button !== 1) return;
  449. const tile = e.target.closest?.(".zen-app-tile[data-app-id]");
  450. if (!tile) return;
  451. e.stopImmediatePropagation();
  452. e.preventDefault();
  453. if (type !== "mousedown") unloadApp(tile.getAttribute("data-app-id"));
  454. };
  455. middleClickHandlers[type] = handler;
  456. document.addEventListener(type, handler, { capture: true });
  457. registerCleanup(() => document.removeEventListener(type, handler, { capture: true }));
  458. });
  459.  
  460. // === STABLE ID-BASED DOCKING TO ESSENTIAL TABS ===================================
  461. // Tiles are re-parented from the sidebar/vertical-bar grid into the
  462. // corresponding essential (pinned) tab, once per app-id, remembered via a
  463. // data attribute on the TAB (not the tile, since renderGrid() recreates
  464. // tile elements from scratch on every re-render — the assignment must
  465. // survive that).
  466. const dockTilesToEssentialTabs = () => {
  467. const essentialTabsRaw = Array.from(document.querySelectorAll(
  468. "#zen-essentials-container .tabbrowser-tab, " +
  469. "#zen-pinned-tab-container .tabbrowser-tab, " +
  470. "#zen-essentials .tabbrowser-tab, " +
  471. "#tabbrowser-tabs[has-pinned-tabs] .tabbrowser-tab[pinned='true'], " +
  472. ".tabbrowser-tab[pinned='true']"
  473. ));
  474.  
  475. // Only trust elements that are genuinely registered tabs. Zen's
  476. // split-view feature creates visual clone(s) of a pinned tab for the
  477. // split preview; those aren't members of gBrowser.tabs. Cloning
  478. // duplicates whatever was inside the tab at clone time — including our
  479. // docked tile — which is what produced the "2nd Zentral icon" bug.
  480. const liveTabs = (typeof gBrowser !== "undefined" && gBrowser?.tabs)
  481. ? Array.from(gBrowser.tabs)
  482. : null;
  483. const essentialTabs = liveTabs
  484. ? essentialTabsRaw.filter((t) => liveTabs.includes(t))
  485. : essentialTabsRaw;
  486.  
  487. const tiles = Array.from(document.querySelectorAll(".zen-app-tile[data-app-id]"));
  488. if (!tiles.length || !essentialTabs.length) return;
  489.  
  490. // Any tile still sitting inside a NON-live tab (a split-view clone) is
  491. // a duplicate — remove it outright.
  492. essentialTabsRaw.forEach((tab) => {
  493. if (!essentialTabs.includes(tab)) {
  494. tab.querySelectorAll(".zen-app-tile[data-app-id]").forEach((t) => t.remove());
  495. }
  496. });
  497.  
  498. // Global de-dupe across all real tabs: an app-id may only have ONE
  499. // tile, in ONE tab, anywhere.
  500. const seenAppIds = new Set();
  501. essentialTabs.forEach((tab) => {
  502. Array.from(tab.querySelectorAll(".zen-app-tile[data-app-id]")).forEach((t) => {
  503. const id = t.getAttribute("data-app-id");
  504. if (!id) return;
  505. if (seenAppIds.has(id)) t.remove();
  506. else seenAppIds.add(id);
  507. });
  508. });
  509.  
  510. const tabsByAssignedId = new Map();
  511. essentialTabs.forEach((tab) => {
  512. const assigned = tab.getAttribute("data-zentral-assigned-app");
  513. if (assigned) tabsByAssignedId.set(assigned, tab);
  514. });
  515.  
  516. tiles.forEach((tile) => {
  517. const id = tile.getAttribute("data-app-id");
  518. if (!id) return;
  519.  
  520. const currentParentTab = tile.closest(".tabbrowser-tab");
  521. if (
  522. currentParentTab &&
  523. essentialTabs.includes(currentParentTab) &&
  524. currentParentTab.getAttribute("data-zentral-assigned-app") === id
  525. ) {
  526. return; // already correctly docked
  527. }
  528.  
  529. const home = tabsByAssignedId.get(id);
  530. if (home) {
  531. if (tile.parentNode !== home) home.appendChild(tile);
  532. return;
  533. }
  534.  
  535. // First time we've ever seen this app-id: claim the first free tab
  536. // and remember it permanently, so it's never re-decided by array
  537. // position again.
  538. const freeTab = essentialTabs.find((t) => !t.getAttribute("data-zentral-assigned-app"));
  539. if (freeTab) {
  540. freeTab.setAttribute("data-zentral-assigned-app", id);
  541. freeTab.appendChild(tile);
  542. }
  543. });
  544. };
  545.  
  546. // === LOADED / DORMANT VISUAL STATE (custom, not part of base) ====================
  547. const syncTileLoadedStates = () => {
  548. const panelSlider = document.getElementById("zen-app-panel-slider");
  549. const loadedAppIds = new Set();
  550.  
  551. if (panelSlider) {
  552. panelSlider.querySelectorAll("browser[data-zentral-app-id]").forEach((b) => {
  553. const id = b.getAttribute("data-zentral-app-id");
  554. if (id) loadedAppIds.add(id);
  555. });
  556. }
  557.  
  558. document.querySelectorAll(".zen-app-tile[data-app-id]").forEach((tile) => {
  559. const id = tile.getAttribute("data-app-id");
  560. const isLoaded = id ? loadedAppIds.has(id) : false;
  561. tile.setAttribute("data-loaded", isLoaded ? "true" : "false");
  562. tile.setAttribute("data-unloaded", isLoaded ? "false" : "true");
  563. // data-active is already maintained live by the base mod's own
  564. // openPanel()/closePanel() across every .zen-app-tile[data-app-id] in
  565. // the document, so we deliberately don't touch it here.
  566. });
  567.  
  568. dockTilesToEssentialTabs();
  569. };
  570.  
  571. // Event-driven docking instead of a 1.5s poll: react to pinned/essential
  572. // tab changes as they happen, debounced so a burst of mutations (e.g.
  573. // dragging several tabs around) only triggers one re-sync.
  574. let dockDebounceTimer = null;
  575. const scheduleDock = (delay = 60) => {
  576. if (dockDebounceTimer) clearTimeout(dockDebounceTimer);
  577. dockDebounceTimer = setTimeout(() => {
  578. dockDebounceTimer = null;
  579. syncTileLoadedStates();
  580. }, delay);
  581. };
  582. registerCleanup(() => { if (dockDebounceTimer) clearTimeout(dockDebounceTimer); });
  583.  
  584. const tabStripEl = document.getElementById("tabbrowser-tabs");
  585. if (tabStripEl) {
  586. const tabStripObserver = new MutationObserver(() => scheduleDock(60));
  587. tabStripObserver.observe(tabStripEl, {
  588. childList: true,
  589. subtree: true,
  590. attributes: true,
  591. attributeFilter: ["pinned", "zen-essential"]
  592. });
  593. registerCleanup(() => tabStripObserver.disconnect());
  594. }
  595.  
  596. // Safety-net fallback for anything an event/observer might miss, at a
  597. // much lower cost than the old 1.5s poll.
  598. const fallbackInterval = setInterval(syncTileLoadedStates, 4000);
  599. registerCleanup(() => clearInterval(fallbackInterval));
  600.  
  601. // Tag each app browser with its app id so "loaded" state can be detected
  602. // via the DOM (the real per-app browser map is a private class field we
  603. // can't read from outside).
  604. patchMethod("getOrCreateAppBrowser", (orig) => function (app) {
  605. const res = orig.call(this, app);
  606. const b = res?.browser || (res instanceof Element ? res : null);
  607. const id = app?.id || app;
  608. if (b && id) b.setAttribute("data-zentral-app-id", id);
  609. setTimeout(syncTileLoadedStates, 50);
  610. return res;
  611. });
  612.  
  613. patchMethod("closeApp", (orig) => function (appId) {
  614. orig.call(this, appId);
  615. setTimeout(syncTileLoadedStates, 50);
  616. });
  617.  
  618. patchMethod("renderGrid", (orig) => function () {
  619. orig.call(this);
  620. setTimeout(syncTileLoadedStates, 50);
  621. });
  622.  
  623. setTimeout(syncTileLoadedStates, 200);
  624.  
  625. // === CLEANUP / UNLOAD ============================================================
  626. // Without this, a Sine hot-reload during development would leave every
  627. // wrapped method and every document-level listener above stacked
  628. // permanently, since `window` (and therefore `apps`) survives a reload.
  629. const performPatchUnload = () => {
  630. cleanupFns.forEach((fn) => { try { fn(); } catch (_) {} });
  631. cleanupFns.length = 0;
  632. window.__zentralOppositeDockPatched = false;
  633. };
  634. if (typeof window.addUnloadListener === "function") {
  635. window.addUnloadListener(performPatchUnload);
  636. } else if (typeof UC_API !== "undefined" && UC_API.addUnloadListener) {
  637. UC_API.addUnloadListener(performPatchUnload);
  638. }
  639. window.addEventListener("unload", performPatchUnload, { once: true });
  640. };
  641.  
  642. if (document.readyState === "complete") {
  643. patchZentralApps();
  644. } else {
  645. window.addEventListener("load", patchZentralApps, { once: true });
  646. }
  647. })();
Advertisement
Add Comment
Please, Sign In to add comment