X.com Enhanced Gallery

Media viewer and download functionality for X.com

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name X.com Enhanced Gallery
// @namespace https://github.com/PiesP/xcom-enhanced-gallery
// @version 1.9.3
// @description Media viewer and download functionality for X.com
// @author PiesP
// @license MIT
// Copyright (c) 2024-2026 X.com Enhanced Gallery Contributors
// @homepageURL https://github.com/PiesP/xcom-enhanced-gallery
// @match https://x.com/*
// @match https://*.x.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_download
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// @connect pbs.twimg.com
// @connect video.twimg.com
// @connect api.twitter.com
// @run-at document-idle
// @supportURL https://github.com/PiesP/xcom-enhanced-gallery/issues
// @icon https://abs.twimg.com/favicons/twitter.3.ico
// @compatible chrome 117+
// @compatible firefox 119+
// @compatible edge 117+
// @compatible safari 17+
// @noframes
// ==/UserScript==
/*
 * Third-Party Licenses
 * ====================
 * Source: https://github.com/PiesP/xcom-enhanced-gallery/tree/v1.9.3/LICENSES
 *
 * MIT License
 *
 * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (M (Lucide)
 * Copyright (c) 2016-2024 Ryan Carniato (Solid.js)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */
(function(){if(typeof document==='undefined')return;var css="@layer xeg.components{:root{--xtt:opacity var(--xdt) var(--xe-s), transform var(--xdt) var(--xe-s), visibility 0ms;--xeg-spacing-gallery:clamp(var(--xs-s), 2.5vw, var(--xs-l));--xeg-spacing-mobile:clamp(var(--xs-xs), 2vw, var(--xs-m));--xeg-spacing-compact:clamp(.25rem, 1.5vw, var(--xs-s));--xth-o:0;--xth-v:hidden;--xth-pe:none}} @media (prefers-reduced-motion:reduce){@layer xeg.components{:root{--xtt:none} .xg-X9gZ{scroll-behavior:auto;transition:none} .xg-meO3{transition:none}}} .xg-X9gZ{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:var(--xz-g, 10000);background:var(--xg-b);display:flex;flex-direction:column;transform:var(--xgh);will-change:opacity, transform;contain:layout style paint;opacity:1;visibility:visible;transition:var(--xten);cursor:default;pointer-events:auto;container-type:size;container-name:gallery-container;scroll-behavior:smooth;overscroll-behavior:none} .xg-meO3{position:fixed;top:0;left:0;right:0;height:auto;z-index:var(--xz-t, 2147483620);opacity:var(--toolbar-opacity, 0);visibility:var(--toolbar-visibility, hidden);display:block;transition:var(--xtt);will-change:transform, opacity, visibility;contain:layout style;transform:var(--xgh);backface-visibility:var(--xbv);pointer-events:var(--toolbar-pointer-events, none);background:transparent;border:none;border-radius:0;margin:0;padding-block-end:var(--xeg-spacing-gallery)} .xg-meO3:is(:hover,:focus-within){--toolbar-opacity:1;--toolbar-visibility:visible;--toolbar-pointer-events:auto} .xg-meO3:focus-within{transition:var(--xtef)} .xg-meO3 *{pointer-events:inherit} .xg-meO3 [data-gallery-element=\"settings-panel\"][data-expanded=\"true\"]{pointer-events:auto} .xg-meO3:has([data-gallery-element=\"settings-panel\"][data-expanded=\"true\"]){--toolbar-opacity:1;--toolbar-visibility:visible;--toolbar-pointer-events:auto} .xg-X9gZ.xg-9abg{cursor:none} .xg-X9gZ.xg-sOsS[data-xeg-gallery=\"true\"][data-xeg-role=\"gallery\"] .xg-meO3{--toolbar-opacity:var(--xth-o, 0);--toolbar-visibility:var(--xth-v, hidden);--toolbar-pointer-events:var(--xth-pe, none)} .xg-X9gZ *{pointer-events:auto} .xg-gmRW{flex:1;display:flex;flex-direction:column;overflow:auto;position:relative;z-index:0;contain:layout style;transform:var(--xgh);overscroll-behavior:contain;scrollbar-gutter:stable;pointer-events:auto;container-type:size;container-name:items-list} .xg-gmRW::-webkit-scrollbar{width:var(--xsw)} .xg-gmRW::-webkit-scrollbar-track{background:transparent} .xg-gmRW::-webkit-scrollbar-thumb{background:var(--xeg-scrollbar-thumb-color, var(--xcn3));border-radius:var( --xsbr );transition:background-color var(--xdn) var(--xe-s)} .xg-gmRW::-webkit-scrollbar-thumb:hover{background:var(--xeg-scrollbar-thumb-hover-color, var(--xcn4))} .xg-X9gZ.xg-9abg .xg-meO3{pointer-events:none;opacity:0;transition:opacity var(--xdf) var(--xe-s)} .xg-X9gZ.xg-9abg [data-xeg-role=\"items-list\"], .xg-X9gZ.xg-9abg .xg-gmRW{pointer-events:auto} .xg-X9gZ.xg-yhK-{justify-content:center;align-items:center} .xg-EfVa{position:relative;margin-bottom:var(--xs-m, 1rem);border-radius:var(--xr-l, .5rem);transition:var(--xten);contain:layout style;transform:var(--xgh)} .xg-LxHL{position:relative;z-index:1} .xg-sfF0{height:calc(100vh - var(--xeg-toolbar-height, 3.75rem));min-height:50vh;pointer-events:none;user-select:none;flex-shrink:0;background:transparent;opacity:0;contain:strict;content-visibility:auto} .xg-gC-m{position:fixed;top:0;left:0;right:0;height:var(--xhzh);z-index:var(--xz-th, 2147483618);background:transparent;pointer-events:auto} .xg-gC-m:hover{z-index:var(--xz-th);background:transparent} .xg-X9gZ.xg-Canm:not([data-settings-expanded=\"true\"]) .xg-gC-m, .xg-X9gZ:has(.xg-meO3:hover):not([data-settings-expanded=\"true\"]) .xg-gC-m{pointer-events:none} .xg-X9gZ.xg-Canm .xg-meO3, .xg-X9gZ:has(.xg-gC-m:hover) .xg-meO3{--toolbar-opacity:1;--toolbar-visibility:visible;--toolbar-pointer-events:auto} .xg-meO3 [class*=\"galleryToolbar\"]{opacity:var(--toolbar-opacity, 0);visibility:var(--toolbar-visibility, hidden);display:flex;pointer-events:var(--toolbar-pointer-events, none)} .xg-meO3 button, .xg-meO3 [role=\"button\"], .xg-meO3 .xg-e06X{pointer-events:auto;position:relative;z-index:10} .xg-fwsr{text-align:center;color:var(--xct-s);max-inline-size:min(25rem, 90vw);padding:clamp(1.875rem, 5vw, 2.5rem)} .xg-fwsr h3{margin:0 0 clamp(.75rem, 2vw, 1rem);font-size:clamp(1.25rem, 4vw, 1.5rem);font-weight:var(--xfw-s);color:var(--xct-p);line-height:var(--xeg-line-height-tight)} .xg-fwsr p{margin:0;font-size:clamp(.875rem, 2.5vw, 1rem);line-height:var(--xlh);color:var(--xct-t)} @container gallery-container (max-width:48rem){.xg-gmRW{padding:var(--xeg-spacing-mobile);gap:var(--xeg-spacing-mobile)} .xg-meO3{padding-block-end:var(--xeg-spacing-mobile)}} @container gallery-container (max-width:30rem){.xg-gmRW{padding:var(--xeg-spacing-compact);gap:var(--xeg-spacing-compact)}} @media (prefers-reduced-motion:reduce){.xg-gmRW{scroll-behavior:auto;will-change:auto;transform:none}} @media (prefers-reduced-motion:reduce){.xg-meO3:hover, .xg-meO3:focus-within{transform:none}} .xg-X9gZ [class*=\"galleryToolbar\"]:hover{--toolbar-opacity:1;--toolbar-pointer-events:auto} .xg-huYo{position:relative;margin-bottom:var(--xs-m);margin-inline:auto;border-radius:var(--xr-l);overflow:visible;transition:var(--xti);cursor:pointer;border:.0625rem solid var(--xcb-p);background:var(--xcbg-s);padding:var(--xs-s);width:fit-content;max-width:100%;text-align:center;display:flex;flex-direction:column;align-items:center;pointer-events:auto;transform:var(--xgh);will-change:transform;contain:layout style} .xg-huYo[data-fit-mode=\"original\"]{max-width:none;flex-shrink:0;width:max-content;align-self:center} .xg-huYo:hover{transform:var(--xhl);background:var(--xc-se);border-color:var(--xbe)} .xg-huYo:focus-visible{border-color:var(--xfic, var(--xcb-p))} .xg-huYo.xg-xm-1{border-color:var(--xbe, var(--xcb-s));transition:var(--xti)} .xg-huYo.xg-xm-1:focus-visible{border-color:var(--xfic, var(--xcb-s))} .xg-huYo.xg-luqi{border-color:var(--xfic, var(--xcb-p));transition:var(--xti)} .xg-8-c8{position:relative;background:var(--xcbg-s);width:fit-content;max-width:100%;margin:0 auto;display:flex;justify-content:center;align-items:center;contain:layout paint} .xg-huYo[data-fit-mode=\"original\"] .xg-8-c8{width:auto;max-width:none} .xg-huYo[data-media-loaded=\"false\"] .xg-8-c8{min-height:var(--xs-3);aspect-ratio:var(--xgi-r, var(--xad))} .xg-lhkE{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:var(--xsk-b);min-height:var(--xs-3)} .xg-6YYD{--xsp-s:var(--xs-l);--xsp-bw:.125rem;--xsp-tc:var(--xcb-p);--xsp-ic:var(--xc-p)} .xg-FWlk, .xg-GUev{display:block;border-radius:var(--xr-m);object-fit:contain;pointer-events:auto;user-select:none;-webkit-user-drag:none;transform:var(--xgh);will-change:opacity;transition:opacity var(--xdn) var(--xe-s)}:is(.xg-FWlk, .xg-GUev).xg-8Z3S{opacity:0}:is(.xg-FWlk, .xg-GUev).xg-y9iP{opacity:1} .xg-GUev{inline-size:100%;overflow:clip}:is(.xg-FWlk, .xg-GUev).xg-yYtG{inline-size:auto;block-size:auto;max-inline-size:none;max-block-size:none;object-fit:none}:is(.xg-FWlk, .xg-GUev).xg-Uc0o{inline-size:auto;block-size:auto;max-inline-size:100%;max-block-size:none;object-fit:scale-down}:is(.xg-FWlk, .xg-GUev).xg-M9Z6{inline-size:auto;block-size:auto;max-inline-size:calc(100vw - var(--xs-l) * 2);max-block-size:var(--xvhc);object-fit:scale-down}:is(.xg-FWlk, .xg-GUev).xg--Mlr{inline-size:auto;block-size:auto;max-inline-size:100%;max-block-size:var(--xvhc);object-fit:contain} .xg-Wno7{font-size:var(--xfs-2);margin-bottom:var(--xs-s)} .xg-8-wi{font-size:var(--xfs-s);text-align:center} .xg-Gswe{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--xc-e-bg);color:var(--xc-e);min-height:var(--xs-3)} .xg-huYo[data-media-loaded=\"false\"][data-fit-mode=\"original\"]{inline-size:min(var(--xgi-w, 100%), 100%);max-inline-size:min(var(--xgi-w, 100%), 100%);max-block-size:min( var(--xgi-h, var(--xs-5)), var(--xvhc) )} .xg-huYo[data-media-loaded=\"false\"][data-fit-mode=\"original\"] .xg-FWlk, .xg-huYo[data-media-loaded=\"false\"][data-fit-mode=\"original\"] .xg-GUev{inline-size:min(var(--xgi-w, 100%), 100%);max-inline-size:min(var(--xgi-w, 100%), 100%);max-block-size:min( var(--xgi-h, var(--xs-5)), var(--xvhc) )} .xg-huYo[data-media-loaded=\"false\"][data-has-intrinsic-size=\"true\"][data-fit-mode=\"fitHeight\"], .xg-huYo[data-media-loaded=\"false\"][data-has-intrinsic-size=\"true\"][data-fit-mode=\"fitContainer\"]{--xgf-ht:min( var(--xgi-h, var(--xs-5)), var(--xvhc) );max-block-size:var(--xgf-ht);inline-size:min( 100%, calc(var(--xgf-ht) * var(--xgi-r, 1)) );max-inline-size:min( 100%, calc(var(--xgf-ht) * var(--xgi-r, 1)) )} .xg-huYo[data-media-loaded=\"false\"][data-has-intrinsic-size=\"true\"]:is( [data-fit-mode=\"fitHeight\"], [data-fit-mode=\"fitContainer\"] ):is(.xg-FWlk, .xg-GUev){max-block-size:var(--xgf-ht);max-inline-size:min( 100%, calc(var(--xgf-ht) * var(--xgi-r, 1)) )} @media (prefers-reduced-motion:reduce){.xg-huYo{will-change:auto;transition:none} .xg-huYo:hover{transform:none}:where(.xg-FWlk, .xg-GUev){will-change:auto;transition:none}} .xg-EeSh{display:flex;flex-direction:column;gap:var(--xse-g);padding:var(--xse-p)} .xg-nm9B{gap:var(--sps)} .xg-PI5C{display:flex;flex-direction:column;gap:var(--xse-cg)} .xg-VUTt{gap:var(--spx)} .xg-vhT3{font-size:var(--xse-lf);font-weight:var(--xse-lw);color:var(--xct-p)} .xg-Y62M{font-size:var(--fsx);color:var(--xct-s);letter-spacing:var(--xeg-letter-spacing-wide);text-transform:uppercase} .xg-jpiS{width:100%;padding:var(--xse-sp);font-size:var(--xse-sf);color:var(--xct-p);background-color:var(--xte-b);border:var(--bwt) solid var(--xt-b);border-radius:var(--xr-m);cursor:pointer;line-height:var(--xeg-line-height-snug);min-height:2.75em;transform:none;overflow:visible;transition:border-color var(--xdf) var(--xe-s), background-color var(--xdf) var(--xe-s), box-shadow var(--xdf) var(--xe-s)} .xg-jpiS:hover{border-color:var(--xcb-h);background-color:var(--xte-bs);box-shadow:0 0 0 2px color-mix(in oklch, var(--xt-b) 20%, transparent 80%)} .xg-jpiS:focus, .xg-jpiS:focus-visible{border-color:var(--xfic);box-shadow:0 0 0 3px color-mix(in oklch, var(--xfic) 25%, transparent 75%)} .xg-jpiS option{padding:.5em .75em;line-height:var(--xlh)} .xg-4eoj{color:var(--xtt-c, var(--xct-p));cursor:pointer;font-size:.875em;font-weight:var(--xfw-m);width:var(--xsb-m);height:var(--xsb-m);min-width:var(--xsb-m);min-height:var(--xsb-m);padding:.5em;aspect-ratio:1;position:relative;overflow:clip;border-radius:var(--xr-m);background:transparent;border:none;transition:var(--xts), transform var(--xdf) var(--xe-s)} .xg-4eoj:focus, .xg-4eoj:focus-visible{background:var(--xte-b, var(--xcn1))} .xg-fLg7{--toolbar-surface-base:var( --xtp-s, var(--xt-s, var(--xcbg-p, Canvas)) );--toolbar-surface-border:var(--xt-b);background:var(--toolbar-surface-base);border:none;border-radius:var(--xr-l);position:fixed;top:1.25em;left:50%;transform:translateX(-50%);z-index:var(--xz-t, 2147483620);display:var(--toolbar-display, inline-flex);align-items:center;justify-content:space-between;height:3em;padding:.5em 1em;gap:0;color:var(--xtt-c, var(--xct-p));visibility:var(--toolbar-visibility, visible);opacity:var(--toolbar-opacity, 1);pointer-events:var(--toolbar-pointer-events, auto);transition:var(--xten);user-select:none;overscroll-behavior:contain} .xg-fLg7.xg-ZpP8, .xg-fLg7.xg-t4eq{border-radius:var(--xr-l) var(--xr-l) 0 0} .xg-fLg7.xg-ojCW{--toolbar-opacity:1;--toolbar-pointer-events:auto;--toolbar-visibility:visible;--toolbar-display:inline-flex} .xg-fLg7.xg-Y6KF, .xg-fLg7.xg-n-ab, .xg-fLg7.xg-bEzl{--toolbar-opacity:1;--toolbar-pointer-events:auto;--toolbar-visibility:visible;--toolbar-display:inline-flex} .xg-f8g4{display:flex;align-items:center;justify-content:center;width:100%;max-width:100%;overflow:hidden} .xg-Ix3j{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:var(--xs-xs);width:100%} .xg-Ix3j \u003E *{flex:0 0 auto} .xg-0EHq{display:flex;align-items:center;justify-content:center;padding-inline:var(--xs-s);min-width:5em} .xg-FKnO{color:var(--xtt-m, var(--xct-p));margin:0 .125em}:where(.xg-4eoj[aria-pressed=\"true\"]){background:var(--xte-bs, var(--xcn2))} .xg-4eoj:disabled{color:var(--xtt-m, var(--xcn4));cursor:not-allowed} @media (hover:hover){.xg-4eoj:hover:not(:disabled){background:var(--xte-b, var(--xcn1));transform:translateY(var(--xb-l))}} .xg-4eoj:active:not(:disabled){background:var(--xte-bs, var(--xcn2));transform:translateY(0)} .xg-njlf{} .xg-AU-d{} .xg-Vn14{} .xg-atmJ{position:relative} .xg-GG86{position:relative;gap:0;min-width:5em;min-height:2.5em;padding-bottom:.5em;box-sizing:border-box} .xg-2cjm{color:var(--xtt-c, var(--xct-p));font-size:var(--xfs-m);font-weight:var(--xfw-s);text-align:center;white-space:nowrap;line-height:1;background:transparent;padding:.25em .5em;border-radius:var(--xr-m);border:none} .xg-JEXm{color:var(--xtt-c, var(--xct-p));font-weight:var(--xeg-font-weight-bold)} .xg-d1et{color:var(--xtt-c, var(--xct-p))} .xg-vB6N{position:absolute;left:50%;bottom:.125em;transform:translateX(-50%);width:3.75em;height:.125em;background:var(--xtp-pt, var(--xcn2));border-radius:var(--xr-s);overflow:clip} .xg-LWQw{width:100%;height:100%;background:var(--xtt-c, var(--xct-p));border-radius:var(--xr-s);transition:var(--xtwn);transform-origin:left} .xg-Q7dU, button.xg-Q7dU{transition:var(--xti);position:relative;z-index:10;pointer-events:auto} .xg-Q7dU[data-selected=\"true\"]{} .xg-Q7dU:focus, .xg-Q7dU:focus-visible{border:none} @media (prefers-reduced-transparency:reduce){.xg-fLg7{background:var(--xtp-s, var(--xt-s))} [data-theme=\"dark\"] .xg-fLg7{background:var(--xtp-s, var(--xt-s))}} @media (prefers-reduced-motion:reduce){.xg-4eoj:hover:not(:disabled), .xg-atmJ:hover:not(:disabled), .xg-Vn14:hover:not(:disabled), .xg-Q7dU:hover{transform:none}}:where(.xg-JcF-, .xg-yRtv){position:absolute;top:100%;left:0;right:0;width:100%;display:flex;flex-direction:column;gap:var(--xs-m);padding:var(--xs-l);max-height:var(--xtp-mh);overflow:hidden;opacity:0;transform:translateY(-.5em);visibility:hidden;pointer-events:none;transition:var(--xtp-t), transform var(--xdn) var(--xe-s), visibility 0s var(--xdn);background:var( --toolbar-surface-base, var(--xtp-s, var(--xt-s)) );border-top:var(--bwt) solid var(--toolbar-surface-border, var(--xt-b));border-radius:0 0 var(--xr-l) var(--xr-l);z-index:var(--xz-tp);will-change:transform, opacity;overscroll-behavior:contain} .xg-JcF-{height:var(--xtp-h)} .xg-yRtv{min-height:var(--xtp-h)}:where(.xg-JcF-, .xg-yRtv).xg-4a2L{height:auto;opacity:1;transform:translateY(0);visibility:visible;pointer-events:auto;border-top-color:var(--toolbar-surface-border, var(--xt-b));transition:var(--xtp-t), transform var(--xdn) var(--xe-s), visibility 0s 0s;z-index:var(--xz-ta)} .xg-w56C{display:flex;flex-direction:column;gap:var(--xs-s)} .xg-rSWg{display:flex;align-items:center;padding-bottom:var(--xs-xs);border-bottom:var(--bwt) solid var(--toolbar-surface-border);margin-bottom:var(--xs-s)} .xg-jd-V{font-size:var(--xfs-s);font-weight:var(--xfw-s);color:var(--xtt-c);text-transform:uppercase;letter-spacing:var(--xeg-letter-spacing-wide)} .xg-jmjG{padding:var(--xs-s) var(--xs-m);font-size:var(--xfs-b);line-height:var(--xeg-line-height-snug);color:var(--xtt-c, var(--xct-p));background:var( --toolbar-surface-base, var(--xtp-s, var(--xt-s)) );border:var(--bwt) solid var(--toolbar-surface-border, var(--xt-b));border-radius:var(--xr-m);white-space:pre-wrap;word-wrap:break-word;overflow-y:auto;overscroll-behavior:contain;max-height:18em;transition:var(--xts);user-select:text;-webkit-user-select:text;cursor:text} .xg-jmjG::-webkit-scrollbar{width:.5em} .xg-jmjG::-webkit-scrollbar-track{background:var(--xts-t, var(--xcn2));border-radius:var(--xr-s)} .xg-jmjG::-webkit-scrollbar-thumb{background:var(--xts-th, var(--xcn4));border-radius:var(--xr-s)} .xg-jmjG::-webkit-scrollbar-thumb:hover{background:var(--xte-bs, var(--xcn5))} .xg-jmjG a{color:var(--xc-p);text-decoration:none;font-weight:var(--xfw-m);padding:.125em .25em;margin:-.125em -.25em;border-radius:var(--xr-xs);overflow-wrap:break-word;transition:color var(--xdf) var(--xe-s), background-color var(--xdf) var(--xe-s);cursor:pointer} .xg-jmjG a:hover{color:var(--xc-ph);background:var(--xte-b);text-decoration:underline;text-decoration-thickness:.0625rem;text-underline-offset:.125em} .xg-jmjG a:focus, .xg-jmjG a:focus-visible{background:var(--xte-bs, var(--xcn2));color:var(--xc-ph);border-radius:var(--xr-xs)} .xg-jmjG a:active{color:var(--xc-p-active)} .xg-0Eeq{display:flex;align-items:center;gap:var(--xs-xs);padding:var(--xs-s);margin-bottom:var(--xs-s);background:var(--xte-bs);border:var(--bwt) solid var(--toolbar-surface-border, var(--xt-b));border-radius:var(--xr-s);transition:var(--xts)} .xg-0Eeq:hover{background:color-mix( in oklch, var(--xte-bs) 85%, var(--xc-p) 15% );border-color:var(--xcb-h)} .xg-AVKe{display:flex;align-items:center;gap:.375em;width:100%;color:var(--xc-p);text-decoration:none;font-size:var(--xfs-s);font-weight:var(--xfw-m);transition:color var(--xdf) var(--xe-s)} .xg-AVKe:hover{color:var(--xc-ph)} .xg-AVKe:focus, .xg-AVKe:focus-visible{outline:.125rem solid var(--xfic);outline-offset:.125rem;border-radius:var(--xr-xs)} .xg-5RjR{flex-shrink:0;width:.875em;height:.875em;stroke:currentColor} .xg-8Stf{flex-shrink:0;color:var(--xtt-m, var(--xct-s));font-weight:var(--xfw-s)} .xg-3pwZ{flex:1;color:var(--xc-p);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .xg-sltl{width:100%;height:var(--bwt);background:color-mix(in oklch, var(--toolbar-surface-border) 60%, transparent 40%);margin:var(--xs-m) 0;border-radius:var(--xr-s)} @layer xeg.features{.xeg-gallery-renderer[data-renderer=\"gallery\"]{display:block;width:0;height:0;overflow:visible} .xeg-gallery-overlay{display:flex;align-items:center;justify-content:center;position:fixed;inset:0;z-index:var(--xz-g, 10000);background:var(--xg-b);opacity:1;transition:opacity var(--xdn) var(--xe-s);pointer-events:auto} .xeg-gallery-container{position:relative;width:100%;height:100%;max-width:100vw;max-height:100vh;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-gutter:stable both-edges}} @layer xeg.tokens, xeg.base, xeg.utilities, xeg.components, xeg.features, xeg.overrides;@layer xeg.tokens{:where(:root, .xeg-theme-scope){--cbw:oklch(1 0 0);--cbb:oklch(0 0 0);--cg0:oklch(.97 .002 206.2);--cg1:oklch(.943 .006 206.2);--cg2:oklch(.896 .006 206.2);--cg3:oklch(.796 .006 206.2);--cg4:oklch(.696 .006 286.3);--cg5:oklch(.598 .006 286.3);--cg6:oklch(.488 .006 286.3);--cg7:oklch(.378 .005 286.3);--cg8:oklch(.306 .005 282);--cg9:oklch(.234 .006 277.8);--spx:.25rem;--sps:.5rem;--spm:1rem;--spl:1.5rem;--sp2:3rem;--rs:.25em;--rm:.375em;--rl:.5em;--rf:50%;--ffp:\"TwitterChirp\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;--fsx:.75rem;--fss:.875rem;--fsb:.9375rem;--fsm:1rem;--fs2:1.5rem;--fwm:500;--fws:600;--fwb:700;--df:150ms;--dn:250ms;--bwt:.0625rem;--line-height-tight:1.25;--line-height-snug:1.375;--lhn:1.5}} @layer xeg.tokens{:where(:root, .xeg-theme-scope){--xcbg-p:var(--cbw);--xcbg-s:var(--cg0);--xeg-color-bg-surface:var(--cbw);--xeg-color-bg-elevated:var(--cbw);--xbgt:var(--xeg-color-bg-surface);--xt-b:var(--xcb-p);--xt-s:var(--xbgt);--xtp-s:var(--xt-s);--xg-bl:var(--xcbg-p);--xg-bd:var(--cg9);--xg-b:var(--xg-bl);--xad:4 \u002F 3;--xct-p:var(--cbb);--xct-s:var(--cg6);--xeg-color-border-default:var(--cg2);--xeg-color-border-emphasis:var(--cg5);--xcb-p:var(--xeg-color-border-default);--xcb-h:var(--cg3);--xcb-s:var(--xeg-color-border-emphasis);--xtt-c:var(--xct-p);--xtt-m:var(--xct-s);--xte-b:color-mix( in oklch, var(--xbgt) 80%, var(--cbw) 20% );--xte-bs:color-mix( in oklch, var(--xbgt) 65%, var(--cbw) 35% );--xte-br:color-mix( in oklch, var(--xt-b) 85%, var(--cbw) 15% );--xtp-pt:color-mix( in oklch, var(--xte-b) 60%, var(--xte-br) 40% );--xts-t:color-mix( in oklch, var(--xte-b) 50%, var(--cbw) 50% );--xts-th:color-mix( in oklch, var(--xte-br) 80%, var(--cbw) 20% );--xc-e:oklch(50% .22 25);--xc-e-bg:oklch(90% .08 25);--xc-p:var(--cg9);--xc-ph:var(--cg7);--xc-p-active:var(--cg8);--xcn1:var(--cg1);--xcn2:var(--cg2);--xcn3:var(--cg3);--xcn4:var(--cg4);--xcn5:var(--cg5);--xct-t:var(--cg5);--xsb-m:2.5em;--xfic:var(--xcb-p);--xfs-s:var(--fss);--xfs-b:var(--fsb);--xfs-m:var(--fsm);--xfs-2:var(--fs2);--xfw-m:var(--fwm);--xfw-s:var(--fws);--xeg-font-weight-bold:var(--fwb);--xeg-line-height-tight:var(--line-height-tight);--xeg-line-height-snug:var(--line-height-snug);--xeg-letter-spacing-wide:.04em;--xdf:var(--df);--xdn:var(--dn);--xdt:var(--dn);--xsu-b:var(--xeg-color-bg-surface);--xsu-br:var(--xeg-color-border-default);--xc-se:var(--xeg-color-bg-elevated);--xsk-b:var(--xcbg-s);--xbe:var(--xeg-color-border-emphasis);--xz-g:2147483600;--xz-th:2147483618;--xz-t:2147483620;--xz-tp:2147483622;--xz-ta:2147483624;--xe-s:cubic-bezier(.4, 0, .2, 1);--xe-d:cubic-bezier(0, 0, .2, 1);--xe-a:cubic-bezier(.4, 0, 1, 1);--xel:linear;--xlh:var(--lhn, 1.5);--xb-l:-.0625rem;--xhl:translateY(-.125rem);--xs-5:var(--sp2);--xr-s:var(--rs);--xr-m:var(--rm);--xr-l:var(--rl);--xr-f:var(--rf);--xeg-scrollbar-thumb-color:var(--cg4);--xeg-scrollbar-thumb-hover-color:var(--cg5)}:where(:root, .xeg-theme-scope)[data-theme=\"light\"]{--xcbg-p:var(--cbw);--xct-p:var(--cbb);--xct-s:var(--cg6);--xg-b:var(--xg-bl)}:where(:root, .xeg-theme-scope)[data-theme=\"dark\"]{--xcbg-p:var(--cg9);--xeg-color-bg-surface:var(--cg9);--xeg-color-bg-elevated:var(--cg7);--xct-p:var(--cbw);--xct-s:var(--cg4);--xbgt:var(--cg8);--xcb-p:var(--cg6);--xt-b:var(--cg6);--xcbg-s:var(--cg8);--xg-b:var(--xg-bd);--xtt-c:var(--xct-p);--xtt-m:var(--cg3);--xte-b:color-mix( in oklch, var(--xbgt) 85%, var(--cbb) 15% );--xte-bs:color-mix( in oklch, var(--xbgt) 70%, var(--cbb) 30% );--xte-br:color-mix( in oklch, var(--xt-b) 75%, var(--cbb) 25% );--xtp-pt:color-mix( in oklch, var(--xt-b) 65%, var(--xbgt) 35% );--xts-t:color-mix( in oklch, var(--xte-b) 80%, var(--cbb) 20% );--xts-th:color-mix( in oklch, var(--xte-br) 85%, var(--cbb) 15% );--xc-p:var(--cg1);--xc-ph:var(--cg2);--xc-p-active:var(--cg3);--xsu-b:var(--cg9);--xsu-br:var(--cg6)} @media (prefers-reduced-motion:reduce){:where(:root, .xeg-theme-scope){--xdf:0ms;--xts:none;--xten:none;--xtef:none;--xti:none;--xtwn:none}}:where(:root, .xeg-theme-scope){--xse-g:var(--spm);--xse-p:var(--spm);--xse-cg:var(--sps);--xse-lf:var(--fss);--xse-lw:var(--fwb);--xse-sf:var(--fss);--xse-sp:var(--sps) var(--spm)}} @layer xeg.tokens{:where(:root, .xeg-theme-scope){--xtp-t:height var(--xdn) var(--xe-s), opacity var(--xdf) var(--xe-s);--xtp-h:0;--xtp-mh:17.5rem;--xsw:.5rem;--xsbr:0;--xhzh:7.5rem;--xsp-sd:1rem;--xsp-bw:.125rem;--xsp-tc:color-mix(in oklch, var(--xcn4) 60%, transparent);--xsp-ic:var(--xc-p, currentColor);--xsp-d:var(--xdn);--xsp-e:var(--xel);--xts:background-color var(--xdf) var(--xe-s), border-color var(--xdf) var(--xe-s), color var(--xdf) var(--xe-s);--xten:transform var(--xdn) var(--xe-s), opacity var(--xdn) var(--xe-s);--xtef:transform var(--xdf) var(--xe-s), opacity var(--xdf) var(--xe-s);--xti:background-color var(--xdf) var(--xe-s), border-color var(--xdf) var(--xe-s), color var(--xdf) var(--xe-s), transform var(--xdf) var(--xe-s);--xtwn:width var(--xdn) var(--xe-s);--xs-xs:var(--spx);--xs-s:var(--sps);--xs-m:var(--spm);--xs-l:var(--spl);--xgh:translateZ(0);--xbv:hidden;--xvhc:90vh} @media (prefers-reduced-transparency:reduce){:where(:root, .xeg-theme-scope){--xsu-b:var(--xcbg-p)}}} @layer xeg.components{.xeg-surface{background:var(--xsu-b);border:.0625rem solid var(--xsu-br);border-radius:var(--xr-l)} .xeg-spinner{display:inline-block;width:var(--xsp-s, var(--xsp-sd));height:var(--xsp-s, var(--xsp-sd));border-radius:var(--xr-f);border:var(--xsp-bw) solid var(--xsp-tc);border-top-color:var(--xsp-ic);animation:xeg-spin var(--xsp-d) var(--xsp-e) infinite;box-sizing:border-box} @media (prefers-reduced-motion:reduce){.xeg-spinner{animation:none}} @keyframes xeg-fade-in{from{opacity:0} to{opacity:1}} @keyframes xeg-fade-out{from{opacity:1} to{opacity:0}} @keyframes xeg-spin{from{transform:rotate(0deg)} to{transform:rotate(360deg)}}} @layer xeg.base{:where(.xeg-gallery-root, .xeg-gallery-root *),:where(.xeg-gallery-root *::before, .xeg-gallery-root *::after){box-sizing:border-box;margin:0;padding:0} .xeg-gallery-root button{border:none;background:none;cursor:pointer;font:inherit;color:inherit} .xeg-gallery-root a{color:inherit;text-decoration:none} .xeg-gallery-root img{max-width:100%;height:auto;display:block} .xeg-gallery-root ul, .xeg-gallery-root ol{list-style:none} .xeg-gallery-root input, .xeg-gallery-root textarea, .xeg-gallery-root select{font:inherit;color:inherit;background:transparent} .xeg-gallery-root::-webkit-scrollbar{width:var(--xsw, .5rem);height:var(--xsw, .5rem)} .xeg-gallery-root::-webkit-scrollbar-track{background:transparent} .xeg-gallery-root::-webkit-scrollbar-thumb{background:var(--xeg-scrollbar-thumb-color);border-radius:var(--xr-s, .25rem)} .xeg-gallery-root::-webkit-scrollbar-thumb:hover{background:var(--xeg-scrollbar-thumb-hover-color)}} @layer xeg.utilities{.xeg-row-center{display:flex;align-items:center} .xeg-inline-center{display:inline-flex;align-items:center;justify-content:center} .xeg-gap-sm{gap:var(--xs-s)}} @layer xeg.utilities{.xeg-fade-in{animation:xeg-fade-in var(--xdn) var(--xe-d);animation-fill-mode:both} .xeg-fade-out{animation:xeg-fade-out var(--xdf) var(--xe-a);animation-fill-mode:both} @media (prefers-reduced-motion:reduce){.xeg-fade-in, .xeg-fade-out{animation:none}}} @layer xeg.base{.xeg-gallery-root{all:unset;box-sizing:border-box;scroll-behavior:smooth;font-family:var(--ffp);font-size:var(--fsb, .9375rem);line-height:var(--xlh, 1.5);color:var(--xct-p, currentColor);position:fixed;inset:0;width:100vw;height:100vh;display:block;z-index:var(--xz-g, 10000);isolation:isolate;contain:style paint;overscroll-behavior:contain;background:var(--xg-b, var(--xcbg-p, Canvas));pointer-events:auto;user-select:none;transform:translateZ(0);will-change:opacity, transform;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}";var s=document.getElementById("xeg-injected-styles");if(!s){s=document.createElement('style');s.id="xeg-injected-styles";(document.head||document.documentElement).appendChild(s);}s.textContent=css;})();
(function() {
var LANGUAGE_CODES = [
"en",
"ko",
"ja"
];
function isBaseLanguageCode(value) {
return value === "en" || value === "ko" || value === "ja";
}
function buildLanguageStringsFromValues(values) {
let i = 0;
const next = () => values[i++];
const built = {
tb: {
prev: next(),
next: next(),
dl: next(),
dlAllCt: next(),
setOpen: next(),
cls: next(),
twTxt: next(),
twPanel: next(),
twUrl: next(),
fitOri: next(),
fitW: next(),
fitH: next(),
fitC: next()
},
st: {
th: next(),
lang: next(),
thAuto: next(),
thLt: next(),
thDk: next(),
langAuto: next(),
langKo: next(),
langEn: next(),
langJa: next()
},
msg: {
err: {
t: next(),
b: next()
},
kb: {
t: next(),
prev: next(),
next: next(),
cls: next(),
toggle: next()
},
dl: {
one: { err: {
t: next(),
b: next()
} },
allFail: {
t: next(),
b: next()
},
part: {
t: next(),
b: next()
}
},
gal: {
emptyT: next(),
emptyD: next(),
itemLbl: next(),
loadFail: next()
}
}
};
return Object.freeze(built);
}
var en = buildLanguageStringsFromValues([
"Previous",
"Next",
"Download",
"Download all {count} files as ZIP",
"Open Settings",
"Close",
"View tweet",
"Tweet text panel",
"View original tweet",
"Original",
"Fit Width",
"Fit Height",
"Fit Window",
"Theme",
"Language",
"Auto",
"Light",
"Dark",
"Auto / 자동 / 自動",
"Korean",
"English",
"Japanese",
"An error occurred",
"An unexpected error occurred: {error}",
"Keyboard shortcuts",
"ArrowLeft: Previous media",
"ArrowRight: Next media",
"Escape: Close gallery",
"?: Show this help",
"Download Failed",
"Could not download the file: {error}",
"Download Failed",
"Failed to download all items.",
"Partial Failure",
"Failed to download {count} items.",
"No media available",
"There are no images or videos to display.",
"Media {index}: {filename}",
"Failed to load {type}"
]);
var ja = buildLanguageStringsFromValues([
"前へ",
"次へ",
"ダウンロード",
"すべての{count}件をZIPでダウンロード",
"設定を開く",
"閉じる",
"ツイートを見る",
"ツイートテキストパネル",
"元のツイートを見る",
"原寸",
"幅に合わせる",
"高さに合わせる",
"ウィンドウに合わせる",
"テーマ",
"Language / 언어 / 言語",
"自動",
"ライト",
"ダーク",
"Auto / 자동 / 自動",
"韓国語",
"英語",
"日本語",
"エラーが発生しました",
"予期しないエラーが発生しました: {error}",
"キーボードショートカット",
"ArrowLeft: 前のメディア",
"ArrowRight: 次のメディア",
"Escape: ギャラリーを閉じる",
"?: このヘルプを表示",
"ダウンロード失敗",
"ファイルを取得できません: {error}",
"ダウンロード失敗",
"すべての項目をダウンロードできませんでした。",
"一部失敗",
"{count}個の項目を取得できませんでした。",
"メディアがありません",
"表示する画像や動画がありません。",
"メディア {index}: {filename}",
"{type} の読み込みに失敗しました"
]);
var TRANSLATION_REGISTRY = {
en,
ko: buildLanguageStringsFromValues([
"이전",
"다음",
"다운로드",
"모든 {count}개 파일을 ZIP으로 다운로드",
"설정 열기",
"닫기",
"트윗 보기",
"트윗 텍스트 패널",
"원본 트윗 보기",
"원본",
"너비 맞춤",
"높이 맞춤",
"창 맞춤",
"테마",
"Language / 언어 / 言語",
"자동",
"라이트",
"다크",
"Auto / 자동 / 自動",
"한국어",
"영어",
"일본어",
"오류가 발생했습니다",
"예상치 못한 오류가 발생했습니다: {error}",
"키보드 단축키",
"ArrowLeft: 이전 미디어",
"ArrowRight: 다음 미디어",
"Escape: 갤러리 닫기",
"?: 이 도움말 표시",
"다운로드 실패",
"파일을 가져올 수 없습니다: {error}",
"다운로드 실패",
"모든 항목을 다운로드할 수 없었습니다.",
"일부 실패",
"{count}개 항목을 가져올 수 없었습니다.",
"미디어 없음",
"표시할 이미지 또는 동영상이 없습니다.",
"미디어 {index}: {filename}",
"{type} 로드 실패"
]),
ja
};
function resolveTranslationValue(dictionary, key) {
const segments = key.split(".");
let current = dictionary;
for (const segment of segments) {
if (current == null || typeof current !== "object") return void 0;
current = current[segment];
}
return typeof current === "string" ? current : void 0;
}
var Translator = class {
bundles = {};
fallbackLanguage;
constructor(options = {}) {
const { bundles = TRANSLATION_REGISTRY, fallbackLanguage = "en" } = options;
this.fallbackLanguage = fallbackLanguage;
for (const [lang, strings] of Object.entries(bundles)) if (strings) this.bundles[lang] = strings;
if (!this.bundles[this.fallbackLanguage]) throw new Error(`Missing fallback language bundle: ${this.fallbackLanguage}`);
}
get languages() {
return [...LANGUAGE_CODES];
}
translate(language, key, params) {
const template = resolveTranslationValue(this.bundles[language] ?? this.bundles[this.fallbackLanguage], key);
if (!template) return key;
if (!params) return template;
return template.replace(/\{(\w+)\}/g, (match, placeholder) => Object.hasOwn(params, placeholder) ? String(params[placeholder]) : match);
}
};
var BASE_PREFIX = "[XEG]";
var hasConsole = typeof console !== "undefined";
var noop = () => {};
var createErrorOnlyLogger = (prefix) => ({
info: noop,
warn: noop,
debug: noop,
trace: noop,
error: (...args) => {
console.error(prefix, ...args);
}
});
var noopLogger = {
info: noop,
warn: noop,
error: noop,
debug: noop,
trace: noop
};
function buildLogger(prefix) {
if (!hasConsole) return noopLogger;
return createErrorOnlyLogger(prefix);
}
function createLogger(config = {}) {
return buildLogger(config.prefix ?? BASE_PREFIX);
}
var logger = createLogger();
function resolveGMAPIs() {
const global = globalThis;
const download = typeof GM_download !== "undefined" ? GM_download : global.GM_download;
const setValue = typeof GM_setValue !== "undefined" ? GM_setValue : global.GM_setValue;
const getValue = typeof GM_getValue !== "undefined" ? GM_getValue : global.GM_getValue;
const deleteValue = typeof GM_deleteValue !== "undefined" ? GM_deleteValue : global.GM_deleteValue;
const listValues = typeof GM_listValues !== "undefined" ? GM_listValues : global.GM_listValues;
const xmlHttpRequest = typeof GM_xmlhttpRequest !== "undefined" ? GM_xmlhttpRequest : global.GM_xmlhttpRequest;
const notification = typeof GM_notification !== "undefined" ? GM_notification : global.GM_notification;
const cookieCandidate = typeof GM_cookie !== "undefined" ? GM_cookie : global.GM_cookie;
return {
download,
setValue,
getValue,
deleteValue,
listValues,
xmlHttpRequest,
cookie: cookieCandidate && typeof cookieCandidate.list === "function" ? cookieCandidate : void 0,
notification
};
}
var cachedGMAPIs = null;
function getResolvedGMAPIsCached() {
if (cachedGMAPIs) return cachedGMAPIs;
cachedGMAPIs = resolveGMAPIs();
return cachedGMAPIs;
}
function asFunction(value) {
return typeof value === "function" ? value : void 0;
}
function resolveGMDownload() {
return getResolvedGMAPIsCached().download;
}
function createUserscriptAPI() {
const resolved = getResolvedGMAPIsCached();
const gmDownload = asFunction(resolved.download);
const gmSetValue = asFunction(resolved.setValue);
const gmGetValue = asFunction(resolved.getValue);
const gmDeleteValue = asFunction(resolved.deleteValue);
const gmListValues = asFunction(resolved.listValues);
const gmXmlHttpRequest = asFunction(resolved.xmlHttpRequest);
const gmNotification = asFunction(resolved.notification);
return {
async download(url, filename) {
if (!gmDownload) throw new Error("GM_download unavailable");
gmDownload(url, filename);
},
async setValue(key, value) {
if (!gmSetValue) throw new Error("GM_setValue unavailable");
await Promise.resolve(gmSetValue(key, value));
},
async getValue(key, defaultValue) {
if (!gmGetValue) throw new Error("GM_getValue unavailable");
return await Promise.resolve(gmGetValue(key, defaultValue));
},
getValueSync(key, defaultValue) {
if (!gmGetValue) return defaultValue;
const value = gmGetValue(key, defaultValue);
return value instanceof Promise ? defaultValue : value;
},
async deleteValue(key) {
if (!gmDeleteValue) throw new Error("GM_deleteValue unavailable");
await Promise.resolve(gmDeleteValue(key));
},
async listValues() {
if (!gmListValues) throw new Error("GM_listValues unavailable");
const values = await Promise.resolve(gmListValues());
return Array.isArray(values) ? values : [];
},
xmlHttpRequest(details) {
if (!gmXmlHttpRequest) throw new Error("GM_xmlhttpRequest unavailable");
return gmXmlHttpRequest(details);
},
notification(details) {
if (!gmNotification) return;
try {
gmNotification(details, void 0);
} catch {}
},
cookie: resolved.cookie
};
}
function getUserscript() {
return createUserscriptAPI();
}
var _persistentStorageInstance = null;
var PersistentStorage = class PersistentStorage {
get userscript() {
return getUserscript();
}
constructor() {}
static getInstance() {
if (!_persistentStorageInstance) _persistentStorageInstance = new PersistentStorage();
return _persistentStorageInstance;
}
async set(key, value) {
if (value === void 0) {
await this.userscript.deleteValue(key);
return;
}
const serialized = typeof value === "string" ? value : JSON.stringify(value);
if (serialized === void 0) {
await this.userscript.deleteValue(key);
return;
}
await this.userscript.setValue(key, serialized);
}
async get(key, defaultValue, options = {}) {
const value = await this.userscript.getValue(key);
if (value === void 0 || value === null) return defaultValue;
try {
return JSON.parse(value);
} catch {
if (options.selfHealOnParseError === true) try {
await this.userscript.deleteValue(key);
} catch {}
return defaultValue;
}
}
async getString(key, defaultValue) {
const value = await this.userscript.getValue(key);
if (value === void 0 || value === null) return defaultValue;
return value;
}
async has(key) {
const value = await this.userscript.getValue(key);
return value !== void 0 && value !== null;
}
getSync(key, defaultValue) {
try {
const value = this.userscript.getValueSync(key);
if (value === void 0 || value === null) return defaultValue;
try {
return JSON.parse(value);
} catch {
return defaultValue;
}
} catch {
return defaultValue;
}
}
async remove(key) {
await this.userscript.deleteValue(key);
}
};
function getPersistentStorage() {
return PersistentStorage.getInstance();
}
var _instance$1 = null;
var LanguageService = class LanguageService {
static STORAGE_KEY = "xeg-language";
_initialized = false;
currentLanguage = "auto";
listeners =  new Set();
storage = getPersistentStorage();
translator;
constructor() {
this.translator = new Translator();
}
static getInstance() {
if (!_instance$1) _instance$1 = new LanguageService();
return _instance$1;
}
async initialize() {
if (this._initialized) return;
try {
const saved = await this.storage.getString(LanguageService.STORAGE_KEY);
const normalized = this.normalizeLanguage(saved);
if (normalized !== this.currentLanguage) {
this.currentLanguage = normalized;
this.notifyListeners(normalized);
}
} catch (error) {}
this._initialized = true;
}
destroy() {
this.listeners.clear();
this._initialized = false;
}
isInitialized() {
return this._initialized;
}
detectLanguage() {
const browserLang = typeof navigator !== "undefined" && navigator.language ? navigator.language.slice(0, 2) : "en";
if (isBaseLanguageCode(browserLang)) return browserLang;
return "en";
}
getCurrentLanguage() {
return this.currentLanguage;
}
setLanguage(language) {
const normalized = this.normalizeLanguage(language);
if (language !== normalized && language !== "auto") {}
if (this.currentLanguage === normalized) return;
this.currentLanguage = normalized;
this.notifyListeners(normalized);
this.persistLanguage(normalized).catch((error) => {});
}
translate(key, params) {
const language = this.getEffectiveLanguage();
return this.translator.translate(language, key, params);
}
onLanguageChange(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
normalizeLanguage(language) {
if (!language) return "auto";
if (language === "auto") return "auto";
if (isBaseLanguageCode(language)) return language;
return "en";
}
notifyListeners(language) {
this.listeners.forEach((listener) => {
try {
listener(language);
} catch (error) {}
});
}
async persistLanguage(language) {
try {
await this.storage.set(LanguageService.STORAGE_KEY, language);
} catch (error) {}
}
getEffectiveLanguage() {
return this.currentLanguage === "auto" ? this.detectLanguage() : this.currentLanguage;
}
};
function normalizeErrorMessage(error) {
if (error instanceof Error) return error.message || error.name || "Error";
if (typeof error === "string") return error;
if (error == null) return String(error);
if (typeof error === "object") {
const record = error;
if (typeof record.message === "string") return record.message;
try {
return JSON.stringify(record);
} catch {
return String(record);
}
}
return String(error);
}
var USER_CANCELLED_MESSAGE = "Download cancelled by user";
var DEFAULT_ABORT_MESSAGE = "This operation was aborted";
function isAbortError(value) {
if (value instanceof DOMException) return value.name === "AbortError" || value.name === "TimeoutError";
if (value instanceof Error) return value.name === "AbortError" || value.name === "TimeoutError";
return false;
}
function isTimeoutError(value) {
if (value instanceof DOMException) return value.name === "TimeoutError";
if (value instanceof Error) return value.name === "TimeoutError";
return false;
}
function attachCause(target, cause) {
if (cause === void 0) return;
try {
target.cause = cause;
} catch {}
}
function createAbortError(message, cause) {
try {
const error = new DOMException(message, "AbortError");
attachCause(error, cause);
return error;
} catch {
const error = new Error(message);
error.name = "AbortError";
attachCause(error, cause);
return error;
}
}
function createUserCancelledAbortError(cause) {
return createAbortError(USER_CANCELLED_MESSAGE, cause);
}
function isUserCancelledAbortError(error) {
if (error instanceof DOMException) return error.name === "AbortError" && error.message === USER_CANCELLED_MESSAGE;
if (error instanceof Error) return error.name === "AbortError" && error.message === USER_CANCELLED_MESSAGE;
return false;
}
function getUserCancelledAbortErrorFromSignal(signal) {
const reason = signal?.reason;
if (isTimeoutError(reason)) return reason;
if (isUserCancelledAbortError(reason)) return reason;
return createUserCancelledAbortError(reason);
}
function getAbortReasonOrAbortErrorFromSignal(signal) {
const reason = signal?.reason;
if (reason instanceof DOMException) return reason;
if (reason instanceof Error) return reason;
return createAbortError(DEFAULT_ABORT_MESSAGE, reason);
}
function promisifyCallback(executor, options) {
return new Promise((resolve, reject) => {
try {
executor((result, error) => {
if (error) {
if (options?.fallback) resolve(Promise.resolve(options.fallback()));
else reject(new Error(String(error)));
return;
}
resolve(result);
});
} catch (error) {
if (options?.fallback) resolve(Promise.resolve(options.fallback()));
else reject(error instanceof Error ? error : new Error(String(error)));
}
});
}
function createDeferred() {
let resolve;
let reject;
return {
promise: new Promise((res, rej) => {
resolve = res;
reject = rej;
}),
resolve,
reject
};
}
var _httpInstance = null;
var HttpRequestService = class HttpRequestService {
defaultTimeout = 1e4;
constructor() {}
async request(method, url, options) {
function createDeferredWithSettledGuard(signal) {
const deferred = createDeferred();
let abortListener = null;
const cleanupAbortListener = () => {
if (abortListener && signal) {
signal.removeEventListener("abort", abortListener);
abortListener = null;
}
};
let settled = false;
const safeResolve = (value) => {
if (settled) return;
settled = true;
try {
cleanupAbortListener();
} catch {}
deferred.resolve(value);
};
const safeReject = (reason) => {
if (settled) return;
settled = true;
try {
cleanupAbortListener();
} catch {}
deferred.reject(reason);
};
return {
promise: deferred.promise,
safeResolve,
safeReject
};
}
function connectAbortSignal(signal, control) {
const abortListener = () => {
control.abort();
};
signal.addEventListener("abort", abortListener, { once: true });
if (signal.aborted) abortListener();
}
const { promise, safeResolve, safeReject } = createDeferredWithSettledGuard(options?.signal);
try {
const userscript = getUserscript();
if (options?.signal?.aborted) {
safeReject(getAbortReasonOrAbortErrorFromSignal(options.signal));
return promise;
}
const details = {
method,
url,
...options?.headers ? { headers: options.headers } : {},
timeout: options?.timeout ?? this.defaultTimeout,
onload: (response) => {
safeResolve({
ok: response.status >= 200 && response.status < 300,
status: response.status,
data: response.response
});
},
onerror: (response) => {
const status = response.status ?? 0;
const errorMessage = status === 0 ? "NET" : `HTTP:${status}`;
const error = new Error(errorMessage);
error.status = status;
safeReject(error);
},
ontimeout: () => {
const error =  new Error("TIMEOUT");
error.status = 0;
safeReject(error);
},
onabort: () => {
safeReject(getAbortReasonOrAbortErrorFromSignal(options?.signal));
}
};
if (options?.responseType) details.responseType = options.responseType;
const data = options?.data;
if (data !== void 0) details.data = data;
const control = userscript.xmlHttpRequest(details);
if (options?.signal) connectAbortSignal(options.signal, control);
} catch (error) {
safeReject(error);
}
return promise;
}
static getInstance() {
if (!_httpInstance) _httpInstance = new HttpRequestService();
return _httpInstance;
}
async get(url, options) {
return this.request("GET", url, options);
}
async post(url, data, options) {
const nextOptions = data === void 0 ? options : {
...options,
data
};
return this.request("POST", url, nextOptions);
}
async put(url, data, options) {
const nextOptions = data === void 0 ? options : {
...options,
data
};
return this.request("PUT", url, nextOptions);
}
async delete(url, options) {
return this.request("DELETE", url, options);
}
async patch(url, data, options) {
const nextOptions = data === void 0 ? options : {
...options,
data
};
return this.request("PATCH", url, nextOptions);
}
};
var TimerManager = class {
timers =  new Set();
setTimeout(callback, delay) {
let id;
id = window.setTimeout(() => {
try {
callback();
} finally {
this.timers.delete(id);
}
}, delay);
this.timers.add(id);
return id;
}
clearTimeout(id) {
if (this.timers.has(id)) {
window.clearTimeout(id);
this.timers.delete(id);
}
}
cleanup() {
this.timers.forEach((id) => window.clearTimeout(id));
this.timers.clear();
}
getActiveTimersCount() {
return this.timers.size;
}
};
var globalTimerManager = new TimerManager();
function getIdleAPIs() {
const hasRIC = typeof requestIdleCallback !== "undefined";
return {
ric: hasRIC ? requestIdleCallback.bind(globalThis) : null,
cic: hasRIC ? cancelIdleCallback.bind(globalThis) : null
};
}
function scheduleIdle(task) {
const { ric, cic } = getIdleAPIs();
if (ric) {
const id = ric(() => {
try {
task();
} catch (error) {
logIdleTaskError(error);
}
});
return { cancel: () => {
cic?.(id);
} };
}
const timerId = globalTimerManager.setTimeout(() => {
try {
task();
} catch (error) {
logIdleTaskError(error);
}
}, 0);
return { cancel: () => {
globalTimerManager.clearTimeout(timerId);
} };
}
var logIdleTaskError = (error) => {};
var PrefetchManager = class {
cache =  new Map();
activeRequests =  new Map();
maxEntries;
constructor(maxEntries = 20) {
this.maxEntries = maxEntries;
}
async prefetch(media, schedule = "idle") {
if (schedule === "immediate") {
await this.prefetchSingle(media.url);
return;
}
scheduleIdle(() => {
this.prefetchSingle(media.url).catch(() => {});
});
}
get(url) {
return this.cache.get(url) ?? null;
}
cancelAll() {
for (const controller of this.activeRequests.values()) controller.abort();
this.activeRequests.clear();
}
clear() {
this.cache.clear();
}
destroy() {
this.cancelAll();
this.clear();
}
async prefetchSingle(url) {
if (this.cache.get(url)) return;
const existingController = this.activeRequests.get(url);
if (existingController) {
const pendingPromise = this.cache.get(url);
if (pendingPromise) {
try {
await pendingPromise;
} catch {}
if (this.cache.has(url)) return;
}
existingController.abort();
this.activeRequests.delete(url);
}
const controller = new AbortController();
this.activeRequests.set(url, controller);
if (this.cache.size >= this.maxEntries) this.evictOldest();
const fetchPromise = HttpRequestService.getInstance().get(url, {
signal: controller.signal,
responseType: "blob"
}).then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.data;
}).finally(() => {
this.activeRequests.delete(url);
});
this.cache.set(url, fetchPromise);
try {
await fetchPromise;
} catch (error) {
if (this.cache.get(url) === fetchPromise) this.cache.delete(url);
}
}
evictOldest() {
const first = this.cache.keys().next();
if (!first.done) {
const url = first.value;
const controller = this.activeRequests.get(url);
if (controller) {
controller.abort();
this.activeRequests.delete(url);
}
this.cache.delete(url);
}
}
};
var CLASSES = {
OVERLAY: "xeg-gallery-overlay",
CONTAINER: "xeg-gallery-container",
ROOT: "xeg-gallery-root",
RENDERER: "xeg-gallery-renderer",
VERTICAL_VIEW: "xeg-vertical-gallery",
ITEM: "xeg-gallery-item"
};
var DATA_ATTRIBUTES = {
GALLERY: "data-xeg-gallery",
ROLE: "data-xeg-role"
};
var SELECTORS = {
OVERLAY: `.${CLASSES.OVERLAY}`,
CONTAINER: `.${CLASSES.CONTAINER}`,
ROOT: `.${CLASSES.ROOT}`,
RENDERER: `.${CLASSES.RENDERER}`,
VERTICAL_VIEW: `.${CLASSES.VERTICAL_VIEW}`,
ITEM: `.${CLASSES.ITEM}`,
DATA_GALLERY: `[${DATA_ATTRIBUTES.GALLERY}]`,
DATA_ROLE: `[${DATA_ATTRIBUTES.ROLE}]`,
ROLE_GALLERY: `[${DATA_ATTRIBUTES.ROLE}="gallery"]`
};
var CSS = {
CLASSES,
DATA_ATTRIBUTES,
SELECTORS,
INTERNAL_SELECTORS: [
SELECTORS.OVERLAY,
SELECTORS.CONTAINER,
SELECTORS.ROOT,
SELECTORS.RENDERER,
SELECTORS.VERTICAL_VIEW,
SELECTORS.ITEM,
SELECTORS.DATA_GALLERY,
SELECTORS.DATA_ROLE,
SELECTORS.ROLE_GALLERY
]
};
var TWEET_SELECTOR = "article[data-testid=\"tweet\"]";
var TWEET_PHOTO_SELECTOR = "[data-testid=\"tweetPhoto\"]";
var TWEET_TEXT_SELECTOR = "[data-testid=\"tweetText\"]";
var VIDEO_PLAYER_SELECTOR = "[data-testid=\"videoPlayer\"]";
var VIDEO_PLAYER_CONTEXT_SELECTOR = `${VIDEO_PLAYER_SELECTOR},[data-testid="videoComponent"],[data-testid="videoPlayerControls"],[data-testid="videoPlayerOverlay"],[role="application"],[aria-label*="Video"]`;
var STATUS_LINK_SELECTOR = "a[href*=\"/status/\"]";
var TWITTER_MEDIA_SELECTOR = "img[src*=\"pbs.twimg.com\"], video[src*=\"video.twimg.com\"]";
var TWEET_CONTAINER_SELECTORS = [TWEET_SELECTOR, "article[role=\"article\"]"];
var MEDIA_CONTAINER_SELECTORS = [TWEET_PHOTO_SELECTOR, VIDEO_PLAYER_SELECTOR];
var VIDEO_CONTAINER_SELECTORS = [VIDEO_PLAYER_SELECTOR, "video"];
var IMAGE_CONTAINER_SELECTORS = [TWEET_PHOTO_SELECTOR, "img[src*=\"pbs.twimg.com\"]"];
var MEDIA_VIEWER_SELECTORS = [
"[data-testid=\"photoViewer\"]",
"[aria-modal=\"true\"][data-testid=\"Drawer\"]",
"[aria-roledescription=\"carousel\"]"
];
var STABLE_MEDIA_CONTAINERS_SELECTORS = MEDIA_CONTAINER_SELECTORS;
var STABLE_MEDIA_VIEWERS_SELECTORS = MEDIA_VIEWER_SELECTORS;
var warnInvalidSelectorOnce = (selector, error) => {};
var logFallbackSelectorMatchOnce = (selector, index, options) => {};
function closestWithFallback(element, selectors, options = {}) {
for (const [index, selector] of selectors.entries()) try {
const match = element.closest(selector);
if (match) {
logFallbackSelectorMatchOnce(selector, index, options);
return match;
}
} catch (error) {
warnInvalidSelectorOnce(selector, error);
}
return null;
}
function getTimestamp() {
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
}
function getElapsedTime(startTime) {
return Math.max(0, getTimestamp() - startTime);
}
function createFailureResult(error, startTime, sourceType, strategy) {
return {
success: false,
mediaItems: [],
clickedIndex: 0,
metadata: {
extractedAt: Date.now(),
sourceType,
strategy,
error,
totalProcessingTime: getElapsedTime(startTime)
},
tweetInfo: null
};
}
var DEFAULT_TRAVERSAL_OPTIONS = {
maxDescendantDepth: 6,
maxAncestorHops: 3
};
function isMediaElement(element) {
if (!element) return false;
return element.tagName === "IMG" || element.tagName === "VIDEO";
}
function findMediaElementInDOM(target, options = {}) {
const { maxDescendantDepth, maxAncestorHops } = {
...DEFAULT_TRAVERSAL_OPTIONS,
...options
};
if (isMediaElement(target)) return target;
const descendant = findMediaDescendant(target, {
includeRoot: false,
maxDepth: maxDescendantDepth
});
if (descendant) return descendant;
let branch = target;
for (let hops = 0; hops < maxAncestorHops && branch; hops++) {
branch = branch.parentElement;
if (!branch) break;
const ancestorMedia = findMediaDescendant(branch, {
includeRoot: true,
maxDepth: maxDescendantDepth
});
if (ancestorMedia) return ancestorMedia;
}
return null;
}
function extractMediaUrlFromElement(element) {
if (element instanceof HTMLImageElement) {
const attr = element.getAttribute("src");
return pickFirstTruthy([
element.currentSrc || null,
attr ? element.src : null,
attr
]);
}
const attr = element.getAttribute("src");
const posterAttr = element.getAttribute("poster");
return pickFirstTruthy([
element.currentSrc || null,
attr ? element.src : null,
attr,
posterAttr ? element.poster : null,
posterAttr
]);
}
function findMediaDescendant(root, { includeRoot, maxDepth }) {
const queue = [{
node: root,
depth: 0
}];
while (queue.length) {
const current = queue.shift();
if (!current) break;
const { node, depth } = current;
if ((includeRoot || node !== root) && isMediaElement(node)) return node;
if (depth >= maxDepth) continue;
for (const child of Array.from(node.children)) if (child instanceof HTMLElement) queue.push({
node: child,
depth: depth + 1
});
}
return null;
}
function pickFirstTruthy(values) {
for (const value of values) if (value) return value;
return null;
}
function extractTweetTextHTML(tweetArticle) {
if (!tweetArticle) return void 0;
try {
const tweetTextElement = tweetArticle.querySelector(TWEET_TEXT_SELECTOR);
if (!tweetTextElement) return void 0;
const text = tweetTextElement.textContent?.trim();
if (!text) return void 0;
return text;
} catch (error) {
logger.error("[tweet] extract failed", error);
return;
}
}
function extractTweetTextHTMLFromClickedElement(element) {
const tweetArticle = closestWithFallback(element, TWEET_CONTAINER_SELECTORS, { debugLabel: "tweet-container" });
if (tweetArticle) return extractTweetTextHTML(tweetArticle);
}
var MEDIA_HOSTS = {
MEDIA_CDN: ["pbs.twimg.com", "video.twimg.com"] };
var MEDIA = {
DOMAINS: [...MEDIA_HOSTS.MEDIA_CDN, "abs.twimg.com"],
HOSTS: MEDIA_HOSTS,
TYPES: {
IMAGE: "image",
VIDEO: "video",
GIF: "gif"
},
EXTENSIONS: {
JPEG: "jpg",
PNG: "png",
WEBP: "webp",
GIF: "gif",
MP4: "mp4",
ZIP: "zip"
},
QUALITY: {
ORIGINAL: "orig",
LARGE: "large",
MEDIUM: "medium",
SMALL: "small"
}
};
var FALLBACK_BASE_URL = "https://x.com";
function tryParseUrl(value, base = FALLBACK_BASE_URL) {
if (value instanceof URL) return value;
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
try {
if (trimmed.startsWith("//")) return new URL(`https:${trimmed}`);
return new URL(trimmed, base);
} catch {
return null;
}
}
function isHostMatching(value, allowedHosts, options = {}) {
if (!Array.isArray(allowedHosts)) return false;
const parsed = value instanceof URL ? value : tryParseUrl(value);
if (!parsed) return false;
const hostname = parsed.hostname.toLowerCase();
const allowSubdomains = options.allowSubdomains === true;
return allowedHosts.some((host) => {
const normalized = host.toLowerCase();
return hostname === normalized || allowSubdomains && hostname.endsWith(`.${normalized}`);
});
}
var RESERVED_TWITTER_PATHS_ARRAY = Object.freeze([
"home",
"explore",
"notifications",
"messages",
"search",
"settings",
"i",
"intent",
"compose",
"hashtag"
]);
var RESERVED_TWITTER_PATHS = new Set(RESERVED_TWITTER_PATHS_ARRAY);
var TWITTER_USERNAME_PATTERN = /^[a-zA-Z0-9_]{1,15}$/u;
var TWITTER_HOSTS = Object.freeze(["twitter.com", "x.com"]);
function extractUsernameFromUrl(url, options = {}) {
if (!url || typeof url !== "string") return null;
try {
let path;
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
const parsed = tryParseUrl(url);
if (!parsed) return null;
if (options.strictHost) {
if (!isHostMatching(parsed, TWITTER_HOSTS, { allowSubdomains: true })) return null;
}
path = parsed.pathname;
} else path = url;
const segments = path.split("/").filter(Boolean);
if (segments.length >= 3 && segments[1] === "status") {
const username = segments[0];
if (!username) return null;
if (RESERVED_TWITTER_PATHS.has(username.toLowerCase())) return null;
if (TWITTER_USERNAME_PATTERN.test(username)) return username;
}
return null;
} catch {
return null;
}
}
var CONTROL_CHARS_REGEX = /[\u0000-\u001F\u007F]/g;
var SCHEME_WHITESPACE_REGEX = /[\u0000-\u001F\u007F\s]+/g;
var EXPLICIT_SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
var MAX_DECODE_ITERATIONS = 3;
var MAX_SCHEME_PROBE_LENGTH = 64;
var DEFAULT_BLOCKED_PROTOCOL_HINTS = [
"javascript:",
"vbscript:",
"file:",
"filesystem:",
"ms-appx:",
"ms-appx-web:",
"about:",
"intent:",
"mailto:",
"tel:",
"sms:",
"wtai:",
"chrome:",
"chrome-extension:",
"opera:",
"resource:",
"data:text",
"data:application",
"data:video",
"data:audio"
];
Object.freeze(new Set([
"http:",
"https:",
"blob:"
]));
function isUrlAllowed(rawUrl, policy) {
if (!rawUrl || typeof rawUrl !== "string") return false;
const normalized = rawUrl.replace(CONTROL_CHARS_REGEX, "").trim();
if (!normalized) return false;
if (startsWithBlockedProtocolHint(normalized, policy.blockedProtocolHints ?? DEFAULT_BLOCKED_PROTOCOL_HINTS)) return false;
const lower = normalized.toLowerCase();
if (lower.startsWith("data:")) return policy.allowDataUrls === true && isAllowedDataUrl(lower, policy.allowedDataMimePrefixes);
if (lower.startsWith("//")) return handleProtocolRelative(normalized, policy);
if (policy.allowFragments && lower.startsWith("#")) return true;
if (!EXPLICIT_SCHEME_REGEX.test(normalized)) return policy.allowRelative === true;
try {
const parsed = new URL(normalized);
return policy.allowedProtocols.has(parsed.protocol);
} catch {
return false;
}
}
function startsWithBlockedProtocolHint(value, hints) {
const probe = value.slice(0, MAX_SCHEME_PROBE_LENGTH);
if (/%(?![0-9A-Fa-f]{2})/.test(probe)) return true;
return buildProbeVariants(probe).some((candidate) => hints.some((hint) => candidate.startsWith(hint)));
}
function buildProbeVariants(value) {
const variants =  new Set();
const base = value.toLowerCase();
variants.add(base);
variants.add(base.replace(SCHEME_WHITESPACE_REGEX, ""));
let decoded = base;
for (let i = 0; i < MAX_DECODE_ITERATIONS; i += 1) try {
decoded = decodeURIComponent(decoded);
variants.add(decoded);
variants.add(decoded.replace(SCHEME_WHITESPACE_REGEX, ""));
} catch {
break;
}
return Array.from(variants.values());
}
function isAllowedDataUrl(lowerCaseValue, allowedPrefixes) {
if (!allowedPrefixes || allowedPrefixes.length === 0) return false;
const [mime] = lowerCaseValue.slice(5).split(";", 1);
if (!mime) return false;
return allowedPrefixes.some((prefix) => mime.startsWith(prefix));
}
function handleProtocolRelative(url, policy) {
if (!policy.allowProtocolRelative) return false;
const fallbackProtocol = policy.allowedProtocols.has("https:") ? "https:" : policy.allowedProtocols.has("http:") ? "http:" : "https:";
try {
const resolved = new URL(`${fallbackProtocol}${url}`);
return policy.allowedProtocols.has(resolved.protocol);
} catch {
return false;
}
}
var MAX_URL_LENGTH = 2048;
var ALLOWED_MEDIA_HOSTS = Object.freeze(MEDIA.HOSTS.MEDIA_CDN);
function isValidMediaUrl(url) {
if (typeof url !== "string" || url.length > MAX_URL_LENGTH) return false;
const parsed = tryParseUrl(url);
if (!parsed) return false;
if (!isHttpProtocol(parsed.protocol)) return false;
if (!isHostMatching(parsed, ALLOWED_MEDIA_HOSTS)) return false;
return isAllowedMediaPath(parsed.hostname, parsed.pathname);
}
var isHttpProtocol = (protocol) => protocol === "https:" || protocol === "http:";
function isAllowedMediaPath(hostname, pathname) {
if (hostname === "pbs.twimg.com") return checkPbsMediaPath(pathname);
if (hostname === "video.twimg.com") return checkVideoMediaPath(pathname);
return false;
}
var checkPbsMediaPath = (pathname) => pathname.startsWith("/media/") || pathname.startsWith("/ext_tw_video_thumb/") || pathname.startsWith("/tweet_video_thumb/") || pathname.startsWith("/video_thumb/") || pathname.startsWith("/amplify_video_thumb/") || pathname.startsWith("/card_img/");
var checkVideoMediaPath = (pathname) => pathname.startsWith("/ext_tw_video/") || pathname.startsWith("/tweet_video/") || pathname.startsWith("/amplify_video/") || pathname.startsWith("/dm_video/");
function findAllMediaInContainer(container) {
const mediaElements = [];
const images = container.querySelectorAll("img[src*=\"pbs.twimg.com\"], img[src*=\"video.twimg.com\"]");
for (const img of images) if (isMediaElement(img)) mediaElements.push(img);
const videos = container.querySelectorAll("video");
for (const video of videos) if (isMediaElement(video)) mediaElements.push(video);
return mediaElements;
}
function createMediaInfoFromDOM(element, tweetInfo, index, tweetTextHTML) {
try {
const mediaUrl = extractMediaUrlFromElement(element);
if (!mediaUrl || !isValidMediaUrl(mediaUrl)) return null;
const mediaType = element.tagName.toLowerCase() === "video" ? "video" : "image";
let width;
let height;
if (element instanceof HTMLImageElement) {
width = element.naturalWidth || element.width || void 0;
height = element.naturalHeight || element.height || void 0;
} else if (element instanceof HTMLVideoElement) {
width = element.videoWidth || element.width || void 0;
height = element.videoHeight || element.height || void 0;
}
return {
id: `${tweetInfo.tweetId}_dom_${index}`,
url: mediaUrl,
type: mediaType,
filename: "",
tweetUsername: tweetInfo.username,
tweetId: tweetInfo.tweetId,
tweetUrl: tweetInfo.tweetUrl,
tweetTextHTML,
originalUrl: mediaUrl,
thumbnailUrl: mediaUrl,
alt: `${mediaType} ${index + 1}`,
...width && height && {
width,
height
},
metadata: {
domIndex: index,
extractionSource: "dom-fallback",
elementTag: element.tagName.toLowerCase()
}
};
} catch (error) {
return null;
}
}
var DOMFallbackExtractor = class {
async extract(tweetInfo, clickedElement, _options, extractionId) {
const startedAt = getTimestamp();
try {
const tweetContainer = closestWithFallback(clickedElement, TWEET_CONTAINER_SELECTORS, { debugLabel: "tweet-container" });
if (!tweetContainer || !(tweetContainer instanceof HTMLElement)) return createFailureResult("No tweet container found", startedAt, "dom-fallback", "dom-extraction-failed");
const tweetTextHTML = extractTweetTextHTMLFromClickedElement(clickedElement);
const mediaElements = findAllMediaInContainer(tweetContainer);
if (mediaElements.length === 0) return createFailureResult("No media elements found in DOM", startedAt, "dom-fallback", "dom-extraction-failed");
const mediaItems = [];
const elementToIndexMap =  new Map();
for (let i = 0; i < mediaElements.length; i++) {
const element = mediaElements[i];
if (!element) continue;
const mediaInfo = createMediaInfoFromDOM(element, tweetInfo, i, tweetTextHTML);
if (mediaInfo) {
elementToIndexMap.set(element, mediaItems.length);
mediaItems.push(mediaInfo);
}
}
if (mediaItems.length === 0) return createFailureResult("No valid media items extracted from DOM", startedAt, "dom-fallback", "dom-extraction-failed");
const clickedMedia = findMediaElementInDOM(clickedElement);
let clickedIndex = 0;
if (clickedMedia) {
const mappedIndex = elementToIndexMap.get(clickedMedia);
if (mappedIndex !== void 0) clickedIndex = mappedIndex;
}
return {
success: true,
mediaItems,
clickedIndex,
metadata: {
extractedAt: Date.now(),
sourceType: "dom-fallback",
strategy: "dom-extraction",
totalProcessingTime: getElapsedTime(startedAt),
domMediaCount: mediaItems.length
},
tweetInfo
};
} catch (error) {
return createFailureResult(normalizeErrorMessage(error), startedAt, "dom-fallback", "dom-extraction-failed");
}
}
};
var DEFAULT_TWEET_ORIGIN = "https://x.com";
var normalizeTweetUrl$1 = (inputUrl) => {
try {
const url = new URL(inputUrl, DEFAULT_TWEET_ORIGIN);
const hostname = url.hostname.toLowerCase();
if (hostname === "twitter.com" || hostname === "www.twitter.com" || hostname === "mobile.twitter.com") {
url.hostname = "x.com";
url.protocol = "https:";
}
if (hostname === "www.x.com") {
url.hostname = "x.com";
url.protocol = "https:";
}
return url.toString();
} catch {
return inputUrl.startsWith("/") ? `${DEFAULT_TWEET_ORIGIN}${inputUrl}` : inputUrl;
}
};
var extractFromElement = (element) => {
const dataId = element.dataset.tweetId;
if (dataId && /^\d+$/.test(dataId)) return {
tweetId: dataId,
username: element.dataset.user ?? "unknown",
tweetUrl: `https://x.com/i/status/${dataId}`,
extractionMethod: "element-attribute",
confidence: .9
};
const href = element.getAttribute("href");
if (href) {
const match = href.match(/\/status\/(\d+)/);
if (match?.[1]) return {
tweetId: match[1],
username: extractUsernameFromUrl(href) ?? "unknown",
tweetUrl: normalizeTweetUrl$1(href),
extractionMethod: "element-href",
confidence: .8
};
}
return null;
};
var extractFromDOM = (element) => {
const container = closestWithFallback(element, TWEET_CONTAINER_SELECTORS, { debugLabel: "tweet-container" });
if (!container) return null;
const statusLink = container.querySelector(STATUS_LINK_SELECTOR);
if (!statusLink) return null;
const href = statusLink.getAttribute("href");
if (!href) return null;
const match = href.match(/\/status\/(\d+)/);
if (!match?.[1]) return null;
return {
tweetId: match[1],
username: extractUsernameFromUrl(href) ?? "unknown",
tweetUrl: normalizeTweetUrl$1(href),
extractionMethod: "dom-structure",
confidence: .85,
metadata: { containerTag: container.tagName.toLowerCase() }
};
};
var extractFromMediaGridItem = (element) => {
const link = element.closest("a");
if (!link) return null;
const href = link.getAttribute("href");
if (!href) return null;
const match = href.match(/\/status\/(\d+)/);
if (!match?.[1]) return null;
return {
tweetId: match[1],
username: extractUsernameFromUrl(href) ?? "unknown",
tweetUrl: normalizeTweetUrl$1(href),
extractionMethod: "media-grid-item",
confidence: .8
};
};
var TweetInfoExtractor = class {
strategies = [
extractFromElement,
extractFromDOM,
extractFromMediaGridItem
];
async extract(element) {
for (const strategy of this.strategies) try {
const result = strategy(element);
if (result && this.isValid(result)) return result;
} catch {}
return null;
}
isValid(info) {
return !!info.tweetId && /^\d+$/.test(info.tweetId) && info.tweetId !== "unknown";
}
};
function safeParseInt(value, radix = 10) {
if (value == null) return 0;
const result = Number.parseInt(value, radix);
return Number.isNaN(result) ? 0 : result;
}
function clamp(value, min = 0, max = 1) {
return Math.min(Math.max(value, min), max);
}
function clampIndex(index, length) {
if (!Number.isFinite(index) || length <= 0) return 0;
return clamp(Math.floor(index), 0, length - 1);
}
var STANDARD_GALLERY_HEIGHT = 720;
var DEFAULT_DIMENSIONS = {
width: 540,
height: STANDARD_GALLERY_HEIGHT
};
function hasValidUrlPrefix(str) {
return /^(?:https?:\/\/|\/\/|\/|\.\/|\.\.\/)/u.test(str);
}
function extractFilenameFromUrl(url) {
if (!url) return null;
const trimmed = url.trim();
if (!trimmed || !hasValidUrlPrefix(trimmed)) return null;
const parsed = tryParseUrl(trimmed, "https://example.invalid");
if (!parsed) return null;
const filename = parsed.pathname.split("/").pop();
return filename && filename.length > 0 ? filename : null;
}
function getMediaDedupKey(media) {
const urlCandidate = typeof media.originalUrl === "string" && media.originalUrl.length > 0 ? media.originalUrl : typeof media.url === "string" && media.url.length > 0 ? media.url : null;
if (!urlCandidate) return null;
const typePrefix = media.type === "image" || media.type === "video" || media.type === "gif" ? `${media.type}:` : "";
const parsed = tryParseUrl(urlCandidate, "https://example.invalid");
if (parsed) {
const host = parsed.hostname;
const path = parsed.pathname;
const format = parsed.searchParams.get("format");
const formatSuffix = format ? `?format=${format}` : "";
if (host && path) return `${typePrefix}${host}${path}${formatSuffix}`;
}
const filename = extractFilenameFromUrl(urlCandidate);
return filename ? `${typePrefix}${filename}` : `${typePrefix}${urlCandidate}`;
}
function removeDuplicateMediaItems(mediaItems) {
if (!mediaItems?.length) return [];
const seen =  new Set();
const result = [];
for (const item of mediaItems) {
if (item == null) continue;
const key = getMediaDedupKey(item);
if (!key) continue;
if (!seen.has(key)) {
seen.add(key);
result.push(item);
}
}
return result;
}
function extractVisualIndexFromUrl(url) {
if (!url) return 0;
const match = url.match(/\/(photo|video)\/(\d+)(?:[?#].*)?$/);
const visualNumber = match?.[2] ? Number.parseInt(match[2], 10) : NaN;
return Number.isFinite(visualNumber) && visualNumber > 0 ? visualNumber - 1 : 0;
}
function sortMediaByVisualOrder(mediaItems) {
if (mediaItems.length <= 1) return mediaItems;
const withVisualIndex = mediaItems.map((media) => {
return {
media,
visualIndex: extractVisualIndexFromUrl(media.expanded_url || "")
};
});
withVisualIndex.sort((a, b) => a.visualIndex - b.visualIndex);
return withVisualIndex.map(({ media }, newIndex) => ({
...media,
index: newIndex
}));
}
function extractDimensionsFromUrl(url) {
if (!url) return null;
const match = url.match(/\/(\d{2,6})x(\d{2,6})(?:\/|\.|$)/);
if (!match) return null;
const width = Number.parseInt(match[1] ?? "", 10);
const height = Number.parseInt(match[2] ?? "", 10);
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
return {
width,
height
};
}
function normalizeDimension(value) {
if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.round(value);
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
if (Number.isFinite(parsed) && parsed > 0) return Math.round(parsed);
}
return null;
}
function normalizeMediaUrl(url) {
if (!url) return null;
const trimmed = url.trim();
if (!trimmed || !hasValidUrlPrefix(trimmed)) return null;
const parsed = tryParseUrl(trimmed, "https://example.invalid");
if (!parsed) return null;
let filename = parsed.pathname.split("/").pop();
if (!filename) return null;
const dotIndex = filename.lastIndexOf(".");
if (dotIndex !== -1) filename = filename.substring(0, dotIndex);
return filename && filename.length > 0 ? filename : null;
}
function scaleAspectRatio(widthRatio, heightRatio) {
if (heightRatio <= 0 || widthRatio <= 0) return DEFAULT_DIMENSIONS;
const scaledHeight = STANDARD_GALLERY_HEIGHT;
return {
width: Math.max(1, Math.round(widthRatio / heightRatio * scaledHeight)),
height: scaledHeight
};
}
function extractDimensionsFromMetadataObject(dimensions) {
if (!dimensions) return null;
const width = normalizeDimension(dimensions.width);
const height = normalizeDimension(dimensions.height);
if (width && height) return {
width,
height
};
return null;
}
function deriveDimensionsFromMetadata(metadata) {
if (!metadata) return null;
const dimensions = extractDimensionsFromMetadataObject(metadata.dimensions);
if (dimensions) return dimensions;
const apiData = metadata.apiData;
if (!apiData) return null;
const originalWidth = normalizeDimension(apiData.original_width ?? apiData.originalWidth);
const originalHeight = normalizeDimension(apiData.original_height ?? apiData.originalHeight);
if (originalWidth && originalHeight) return {
width: originalWidth,
height: originalHeight
};
const downloadUrl = apiData.download_url;
if (typeof downloadUrl === "string" && downloadUrl) {
const fromDownloadUrl = extractDimensionsFromUrl(downloadUrl);
if (fromDownloadUrl) return fromDownloadUrl;
}
const previewUrl = apiData.preview_url;
if (typeof previewUrl === "string" && previewUrl) {
const fromPreviewUrl = extractDimensionsFromUrl(previewUrl);
if (fromPreviewUrl) return fromPreviewUrl;
}
const aspectRatio = apiData.aspect_ratio;
if (Array.isArray(aspectRatio) && aspectRatio.length >= 2) {
const ratioWidth = normalizeDimension(aspectRatio[0]);
const ratioHeight = normalizeDimension(aspectRatio[1]);
if (ratioWidth && ratioHeight) return scaleAspectRatio(ratioWidth, ratioHeight);
}
return null;
}
function deriveDimensionsFromMediaUrls(media) {
const candidates = [
media.url,
media.originalUrl,
media.thumbnailUrl
];
for (const candidate of candidates) if (typeof candidate === "string" && candidate) {
const dimensions = extractDimensionsFromUrl(candidate);
if (dimensions) return dimensions;
}
return null;
}
function resolveMediaDimensionsWithIntrinsicFlag(media) {
if (!media) return {
dimensions: DEFAULT_DIMENSIONS,
hasIntrinsicSize: false
};
const directWidth = normalizeDimension(media.width);
const directHeight = normalizeDimension(media.height);
if (directWidth && directHeight) return {
dimensions: {
width: directWidth,
height: directHeight
},
hasIntrinsicSize: true
};
const fromMetadata = deriveDimensionsFromMetadata(media.metadata);
if (fromMetadata) return {
dimensions: fromMetadata,
hasIntrinsicSize: true
};
const fromUrls = deriveDimensionsFromMediaUrls(media);
if (fromUrls) return {
dimensions: fromUrls,
hasIntrinsicSize: true
};
return {
dimensions: DEFAULT_DIMENSIONS,
hasIntrinsicSize: false
};
}
function toRem(pixels) {
return `${(pixels / 16).toFixed(4)}rem`;
}
function createIntrinsicSizingStyle(dimensions) {
const ratio = dimensions.height > 0 ? dimensions.width / dimensions.height : 1;
return {
"--xeg-aspect-default": `${dimensions.width} / ${dimensions.height}`,
"--xeg-gallery-item-intrinsic-width": toRem(dimensions.width),
"--xeg-gallery-item-intrinsic-height": toRem(dimensions.height),
"--xeg-gallery-item-intrinsic-ratio": ratio.toFixed(6)
};
}
function adjustClickedIndexAfterDeduplication(originalItems, uniqueItems, originalClickedIndex) {
if (uniqueItems.length === 0) return 0;
const clickedItem = originalItems[clampIndex(originalClickedIndex, originalItems.length)];
if (!clickedItem) return 0;
const clickedKey = getMediaDedupKey(clickedItem);
if (!clickedKey) return 0;
const newIndex = uniqueItems.findIndex((item) => {
return getMediaDedupKey(item) === clickedKey;
});
return newIndex >= 0 ? newIndex : 0;
}
var resolveDimensionsFromApiMedia = (apiMedia) => {
const widthFromOriginal = normalizeDimension(apiMedia.original_width);
const heightFromOriginal = normalizeDimension(apiMedia.original_height);
return widthFromOriginal && heightFromOriginal ? {
width: widthFromOriginal,
height: heightFromOriginal
} : null;
};
var createMediaInfoFromAPI = (apiMedia, tweetInfo, index, tweetTextHTML) => {
try {
const mediaType = apiMedia.type === "photo" ? "image" : "video";
const dimensions = resolveDimensionsFromApiMedia(apiMedia);
const metadata = {
apiIndex: index,
apiData: apiMedia
};
if (dimensions) metadata.dimensions = dimensions;
const username = apiMedia.screen_name || tweetInfo.username;
return {
id: `${tweetInfo.tweetId}_api_${index}`,
url: apiMedia.download_url,
type: mediaType,
filename: "",
tweetUsername: username,
tweetId: tweetInfo.tweetId,
tweetUrl: tweetInfo.tweetUrl,
tweetText: apiMedia.tweet_text,
tweetTextHTML,
originalUrl: apiMedia.download_url,
thumbnailUrl: apiMedia.preview_url,
alt: `${mediaType} ${index + 1}`,
...dimensions && {
width: dimensions.width,
height: dimensions.height
},
metadata
};
} catch (error) {
return null;
}
};
async function convertAPIMediaToMediaInfo(apiMedias, tweetInfo, tweetTextHTML) {
const mediaItems = [];
for (let i = 0; i < apiMedias.length; i++) {
const apiMedia = apiMedias[i];
if (!apiMedia) continue;
const mediaInfo = createMediaInfoFromAPI(apiMedia, tweetInfo, i, tweetTextHTML);
if (mediaInfo) mediaItems.push(mediaInfo);
}
return mediaItems;
}
var TWITTER_API_CONFIG = {
GUEST_AUTHORIZATION: "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
TWEET_RESULT_BY_REST_ID_QUERY_ID: "zAz9764BcLZOJ0JU2wrd1A",
USER_BY_SCREEN_NAME_QUERY_ID: "1VOOyvKkiI3FMmkeDNxM9A",
SUPPORTED_HOSTS: ["x.com", "twitter.com"],
DEFAULT_HOST: "x.com"
};
function serializeQueryParams(value) {
return typeof value === "string" ? value : JSON.stringify(value);
}
function buildTweetResultByRestIdUrl(args) {
const { host, queryId, variables, features, fieldToggles } = args;
const urlObj = new URL(`https://${host}/i/api/graphql/${queryId}/TweetResultByRestId`);
urlObj.searchParams.set("variables", serializeQueryParams(variables));
urlObj.searchParams.set("features", serializeQueryParams(features));
urlObj.searchParams.set("fieldToggles", serializeQueryParams(fieldToggles));
return urlObj.toString();
}
function getLocationLike() {
try {
return globalThis.location;
} catch {
return;
}
}
function getSafeLocationValue(key) {
const location = getLocationLike();
if (!location) return;
try {
return location[key];
} catch {
return;
}
}
function getSafeOrigin() {
return getSafeLocationValue("origin");
}
function getSafeHref() {
return getSafeLocationValue("href");
}
function getSafeHostname() {
return getSafeLocationValue("hostname");
}
function getSafeLocationHeaders() {
const referer = getSafeHref();
const origin = getSafeOrigin();
if (!referer && !origin) return {};
return {
...referer ? { referer } : {},
...origin ? { origin } : {}
};
}
async function delay(ms, signal) {
if (ms <= 0) return;
if (signal?.aborted) throw getAbortReasonOrAbortErrorFromSignal(signal);
return new Promise((resolve, reject) => {
const timerId = globalTimerManager.setTimeout(() => {
cleanup();
resolve();
}, ms);
const onAbort = () => {
cleanup();
reject(getAbortReasonOrAbortErrorFromSignal(signal));
};
const cleanup = () => {
globalTimerManager.clearTimeout(timerId);
signal?.removeEventListener("abort", onAbort);
};
signal?.addEventListener("abort", onAbort, { once: true });
});
}
function getExponentialBackoffDelayMs(attempt, baseDelayMs) {
return baseDelayMs * 2 ** attempt;
}
var DEFAULT_OPTIONS = {
maxAttempts: 3,
baseDelayMs: 200,
maxDelayMs: 1e4
};
var calculateBackoff = (attempt, baseDelayMs, maxDelayMs) => {
const exponentialDelay = getExponentialBackoffDelayMs(attempt, baseDelayMs);
const totalDelay = exponentialDelay + Math.random() * .25 * exponentialDelay;
return Math.min(Math.floor(totalDelay), maxDelayMs);
};
async function withRetry(operation, options = {}) {
const { maxAttempts = DEFAULT_OPTIONS.maxAttempts, baseDelayMs = DEFAULT_OPTIONS.baseDelayMs, maxDelayMs = DEFAULT_OPTIONS.maxDelayMs, signal, onRetry, shouldRetry = () => true } = options;
let lastError;
let attempt = 0;
while (attempt < maxAttempts) {
if (signal?.aborted) return {
success: false,
error: signal.reason ?? new DOMException("Operation was aborted", "AbortError"),
attempts: attempt
};
try {
return {
success: true,
data: await operation(),
attempts: attempt + 1
};
} catch (error) {
lastError = error;
attempt++;
if (isAbortError(error)) return {
success: false,
error,
attempts: attempt
};
if (!shouldRetry(error)) return {
success: false,
error,
attempts: attempt
};
if (attempt >= maxAttempts) break;
const delayMs = calculateBackoff(attempt - 1, baseDelayMs, maxDelayMs);
onRetry?.(attempt, error, delayMs);
try {
await delay(delayMs, signal);
} catch (delayError) {
if (isAbortError(delayError)) return {
success: false,
error: delayError,
attempts: attempt
};
throw delayError;
}
}
}
return {
success: false,
error: lastError,
attempts: attempt
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function cx(...inputs) {
const classes = [];
for (const input of inputs) {
if (!input) continue;
if (typeof input === "string" || typeof input === "number") classes.push(String(input));
else if (Array.isArray(input)) {
const nested = cx(...input);
if (nested) classes.push(nested);
} else if (typeof input === "object") {
for (const [key, value] of Object.entries(input)) if (value) classes.push(key);
}
}
return classes.join(" ");
}
var cachedCookieAPI;
var decode = (value) => {
if (!value) return void 0;
try {
return decodeURIComponent(value);
} catch {
return value;
}
};
var resolveCookieAPI = () => {
try {
return getUserscript().cookie ?? null;
} catch (error) {
return null;
}
};
var getCookieAPI = () => {
if (cachedCookieAPI === void 0) cachedCookieAPI = resolveCookieAPI();
return cachedCookieAPI;
};
var listFromDocument = (options) => {
if (typeof document === "undefined" || typeof document.cookie !== "string") return [];
const domain = typeof document.location?.hostname === "string" ? document.location.hostname : void 0;
const records = document.cookie.split(";").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
const [rawName, ...rest] = entry.split("=");
const nameDecoded = decode(rawName);
if (!nameDecoded) return null;
return {
name: nameDecoded,
value: decode(rest.join("=")) ?? "",
path: "/",
session: true,
...domain ? { domain } : {}
};
}).filter((record) => !!record);
return options?.name ? records.filter((r) => r.name === options.name) : records;
};
async function listCookies(options) {
const gmCookie = getCookieAPI();
if (!gmCookie?.list) return listFromDocument(options);
return promisifyCallback((callback) => gmCookie?.list(options, (cookies, error) => {
if (error) ;
callback(error ? void 0 : (cookies ?? []).map((c) => ({ ...c })), error);
}), { fallback: () => listFromDocument(options) });
}
async function getCookieValue(name, options) {
if (!name) return void 0;
if (getCookieAPI()?.list) {
const value = (await listCookies({
...options,
name
}))[0]?.value;
if (value) return value;
}
return getCookieValueSync(name);
}
function getCookieValueSync(name) {
if (!name) return void 0;
if (typeof document === "undefined" || typeof document.cookie !== "string") return;
const pattern = new RegExp(`(?:^|;\\s*)${escapeRegExp(name)}=([^;]*)`);
return decode(document.cookie.match(pattern)?.[1]);
}
var MAX_RETRY_ATTEMPTS = 3;
var BASE_RETRY_DELAY_MS = 100;
var _csrfToken;
var _tokensInitialized = false;
var _initPromise = null;
var getBackoffDelay = (attempt) => getExponentialBackoffDelayMs(attempt, BASE_RETRY_DELAY_MS);
var fetchTokenWithRetry = async () => {
const syncToken = getCookieValueSync("ct0");
if (syncToken) return syncToken;
for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) try {
const value = await getCookieValue("ct0");
if (value) return value;
if (attempt < MAX_RETRY_ATTEMPTS - 1) await delay(getBackoffDelay(attempt));
} catch (error) {
if (attempt < MAX_RETRY_ATTEMPTS - 1) await delay(getBackoffDelay(attempt));
}
};
var initializeTokensSync = () => {
if (_tokensInitialized) return;
const syncToken = getCookieValueSync("ct0");
if (syncToken) {
_csrfToken = syncToken;
_tokensInitialized = true;
}
};
var initTokens = async () => {
if (_tokensInitialized && _csrfToken) return _csrfToken;
if (_initPromise) return _initPromise;
_initPromise = (async () => {
try {
const token = await fetchTokenWithRetry();
_csrfToken = token;
_tokensInitialized = true;
return token;
} finally {
_initPromise = null;
}
})();
return _initPromise;
};
function getCsrfToken() {
initializeTokensSync();
return _csrfToken;
}
async function getCsrfTokenAsync() {
if (_tokensInitialized && _csrfToken) return _csrfToken;
return initTokens();
}
var FALLBACK_BEARER_TOKEN = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
function resolveBearerToken() {
try {
const nextDataScript = document.getElementById("__NEXT_DATA__");
if (nextDataScript?.textContent) {
const nextData = JSON.parse(nextDataScript.textContent);
const token = nextData?.props?.pageProps?.token?.Bearer ?? nextData?.props?.token?.Bearer;
if (token && typeof token === "string") return `Bearer ${token}`;
}
} catch {}
return FALLBACK_BEARER_TOKEN;
}
function resolveDimensions(media, mediaUrl) {
const dimensionsFromUrl = extractDimensionsFromUrl(mediaUrl);
const widthFromOriginal = normalizeDimension(media.original_info?.width);
const heightFromOriginal = normalizeDimension(media.original_info?.height);
const widthFromUrl = dimensionsFromUrl?.width;
const heightFromUrl = dimensionsFromUrl?.height;
return {
...widthFromOriginal ?? widthFromUrl ? { width: widthFromOriginal ?? widthFromUrl } : {},
...heightFromOriginal ?? heightFromUrl ? { height: heightFromOriginal ?? heightFromUrl } : {}
};
}
var removeUrlTokensFromText = (text, urls) => {
let result = text;
for (const url of urls) {
if (!url) continue;
const token = escapeRegExp(url);
const re = new RegExp(`(^|\\s+)${token}(?=\\s+|$)`, "g");
result = result.replace(re, (_match, leadingWs) => leadingWs);
}
return result.replace(/[ \t\f\v\u00A0]{2,}/g, " ").replace(/ ?\n ?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
};
var resolveAspectRatio = (media, dimensions) => {
const aspectRatioValues = Array.isArray(media.video_info?.aspect_ratio) ? media.video_info?.aspect_ratio : void 0;
const aspectRatioWidth = normalizeDimension(aspectRatioValues?.[0]);
const aspectRatioHeight = normalizeDimension(aspectRatioValues?.[1]);
if (aspectRatioWidth && aspectRatioHeight) return [aspectRatioWidth, aspectRatioHeight];
if (dimensions.width && dimensions.height) return [dimensions.width, dimensions.height];
};
var getPhotoHighQualityUrl = (mediaUrlHttps) => {
if (!mediaUrlHttps) return mediaUrlHttps;
const isAbsolute = /^(https?:)?\/\//i.test(mediaUrlHttps);
const parsed = tryParseUrl(mediaUrlHttps, "https://pbs.twimg.com");
if (!parsed) {
const [pathPart = "", existingQuery = ""] = mediaUrlHttps.split("?");
const pathMatch = pathPart.match(/\.(jpe?g|png)$/i);
if (!pathMatch) return mediaUrlHttps;
const ext = (pathMatch[1] ?? "").toLowerCase();
const params = new URLSearchParams(existingQuery);
if (!Array.from(params.keys()).some((k) => k.toLowerCase() === "format")) params.set("format", ext);
params.set("name", "orig");
const query = params.toString();
return query ? `${pathPart}?${query}` : pathPart;
}
const hasParamCaseInsensitive = (key) => Array.from(parsed.searchParams.keys()).some((k) => k.toLowerCase() === key);
const setParamCaseInsensitive = (key, value) => {
for (const k of Array.from(parsed.searchParams.keys())) if (k !== key && k.toLowerCase() === key) parsed.searchParams.delete(k);
parsed.searchParams.set(key, value);
};
const pathMatch = parsed.pathname.match(/\.(jpe?g|png)$/i);
if (!pathMatch) return mediaUrlHttps;
const ext = (pathMatch[1] ?? "").toLowerCase();
if (!hasParamCaseInsensitive("format")) setParamCaseInsensitive("format", ext);
setParamCaseInsensitive("name", "orig");
if (isAbsolute) return parsed.toString();
return `${parsed.pathname}${parsed.search}`;
};
var getVideoHighQualityUrl = (media) => {
const mp4Variants = (media.video_info?.variants ?? []).filter((v) => v.content_type === "video/mp4");
if (mp4Variants.length === 0) return null;
return mp4Variants.reduce((best, current) => {
return (current.bitrate ?? 0) > (best.bitrate ?? 0) ? current : best;
}).url;
};
function getHighQualityMediaUrl(media) {
if (media.type === "photo") return getPhotoHighQualityUrl(media.media_url_https) ?? null;
if (media.type === "video" || media.type === "animated_gif") return getVideoHighQualityUrl(media);
return null;
}
var createMediaEntry = (media, mediaUrl, screenName, tweetId, tweetText, index, sourceLocation) => {
const mediaType = media.type === "animated_gif" ? "video" : media.type;
const dimensions = resolveDimensions(media, mediaUrl);
const aspectRatio = resolveAspectRatio(media, dimensions);
return {
screen_name: screenName,
tweet_id: tweetId,
download_url: mediaUrl,
type: mediaType,
typeOriginal: media.type,
index,
preview_url: media.media_url_https,
media_id: media.id_str,
media_key: media.media_key ?? "",
expanded_url: media.expanded_url ?? "",
short_expanded_url: media.display_url ?? "",
short_tweet_url: media.url ?? "",
tweet_text: tweetText,
sourceLocation,
...dimensions.width && { original_width: dimensions.width },
...dimensions.height && { original_height: dimensions.height },
...aspectRatio && { aspect_ratio: aspectRatio }
};
};
function extractMediaFromTweet(tweetResult, tweetUser, sourceLocation = "original") {
const quotedResult = tweetResult.quoted_status_result?.result;
const parseTarget = sourceLocation === "quoted" && quotedResult ? quotedResult : tweetResult;
if (!parseTarget.extended_entities?.media) return [];
const mediaItems = [];
const screenName = tweetUser.screen_name ?? "";
const tweetId = parseTarget.rest_id ?? parseTarget.id_str ?? "";
const inlineMedia = parseTarget.note_tweet?.note_tweet_results?.result?.media?.inline_media;
const inlineMediaOrder =  new Map();
if (Array.isArray(inlineMedia)) {
for (const item of inlineMedia) if (item.media_id && typeof item.index === "number") inlineMediaOrder.set(item.media_id, item.index);
}
const orderedMedia = (() => {
const mediaList = parseTarget.extended_entities?.media ?? [];
if (inlineMediaOrder.size === 0) return mediaList;
return mediaList.map((media, originalIndex) => ({
media,
originalIndex
})).sort((left, right) => {
const leftInline = inlineMediaOrder.get(left.media.id_str);
const rightInline = inlineMediaOrder.get(right.media.id_str);
if (leftInline !== void 0 && rightInline !== void 0) return leftInline - rightInline;
if (leftInline !== void 0) return -1;
if (rightInline !== void 0) return 1;
return left.originalIndex - right.originalIndex;
}).map((entry) => entry.media);
})();
const normalizedTweetText = removeUrlTokensFromText((parseTarget.full_text ?? "").trim(), orderedMedia.map((m) => m.url).filter((u) => typeof u === "string" && u.length > 0));
for (let index = 0; index < orderedMedia.length; index++) {
const media = orderedMedia[index];
if (!media?.type) continue;
if (!media.id_str) continue;
if (!media.media_url_https) continue;
try {
const mediaUrl = getHighQualityMediaUrl(media);
if (!mediaUrl) continue;
const entry = createMediaEntry(media, mediaUrl, screenName, tweetId, normalizedTweetText, index, sourceLocation);
mediaItems.push(entry);
} catch (error) {}
}
return mediaItems;
}
function normalizeLegacyTweet(tweet) {
const mutableTweet = tweet;
if (mutableTweet.legacy) {
if (!mutableTweet.extended_entities && mutableTweet.legacy.extended_entities) mutableTweet.extended_entities = mutableTweet.legacy.extended_entities;
if (!mutableTweet.full_text && mutableTweet.legacy.full_text) mutableTweet.full_text = mutableTweet.legacy.full_text;
if (!mutableTweet.id_str && mutableTweet.legacy.id_str) mutableTweet.id_str = mutableTweet.legacy.id_str;
}
const noteTweetText = mutableTweet.note_tweet?.note_tweet_results?.result?.text;
if (noteTweetText) mutableTweet.full_text = noteTweetText;
}
function normalizeLegacyUser(user) {
const mutableUser = user;
if (mutableUser.legacy) {
if (!mutableUser.screen_name && mutableUser.legacy.screen_name) mutableUser.screen_name = mutableUser.legacy.screen_name;
if (!mutableUser.name && mutableUser.legacy.name) mutableUser.name = mutableUser.legacy.name;
}
}
var resolveTwitterApiHost = (hostname, supportedHosts, defaultHost) => {
if (!hostname) return defaultHost;
const parts = hostname.toLowerCase().split(".");
if (parts.length < 2) return defaultHost;
const baseDomain = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
for (const host of supportedHosts) if (baseDomain === host) return host;
return defaultHost;
};
var getSafeHost = () => resolveTwitterApiHost(getSafeHostname(), TWITTER_API_CONFIG.SUPPORTED_HOSTS, TWITTER_API_CONFIG.DEFAULT_HOST);
var TwitterAPI = class TwitterAPI {
static async getTweetMedias(tweetId) {
const url = TwitterAPI.createTweetEndpointUrl(tweetId);
const json = await TwitterAPI.apiRequest(url);
if (!json.data?.tweetResult?.result) return [];
let tweetResult = json.data.tweetResult.result;
if (tweetResult.tweet) tweetResult = tweetResult.tweet;
const tweetUser = tweetResult.core?.user_results?.result;
normalizeLegacyTweet(tweetResult);
if (!tweetUser) return [];
normalizeLegacyUser(tweetUser);
let result = extractMediaFromTweet(tweetResult, tweetUser, "original");
result = sortMediaByVisualOrder(result);
if (tweetResult.quoted_status_result?.result) {
let quotedTweet = tweetResult.quoted_status_result.result;
if (quotedTweet.tweet) quotedTweet = quotedTweet.tweet;
const quotedUser = quotedTweet.core?.user_results?.result;
if (quotedTweet && quotedUser) {
normalizeLegacyTweet(quotedTweet);
normalizeLegacyUser(quotedUser);
const sortedQuotedMedia = sortMediaByVisualOrder(extractMediaFromTweet(quotedTweet, quotedUser, "quoted"));
const adjustedResult = result.map((media) => ({
...media,
index: media.index + sortedQuotedMedia.length
}));
result = [...sortedQuotedMedia, ...adjustedResult];
}
}
return result;
}
static async apiRequest(url) {
const csrfToken = await getCsrfTokenAsync() ?? getCsrfToken() ?? "";
const authorization = resolveBearerToken() ?? TWITTER_API_CONFIG.GUEST_AUTHORIZATION;
const headers = new Headers({
authorization,
"x-csrf-token": csrfToken,
"x-twitter-client-language": "en",
"x-twitter-active-user": "yes",
"content-type": "application/json",
"x-twitter-auth-type": "OAuth2Session"
});
const locationHeaders = getSafeLocationHeaders();
if (locationHeaders.referer) headers.append("referer", locationHeaders.referer);
if (locationHeaders.origin) headers.append("origin", locationHeaders.origin);
try {
const response = await HttpRequestService.getInstance().get(url, {
headers: Object.fromEntries(headers.entries()),
responseType: "json"
});
if (!response.ok) throw new Error(`TW:${response.status}`);
return response.data;
} catch (error) {
throw error;
}
}
static createTweetEndpointUrl(tweetId) {
return buildTweetResultByRestIdUrl({
host: getSafeHost(),
queryId: TWITTER_API_CONFIG.TWEET_RESULT_BY_REST_ID_QUERY_ID,
variables: {
tweetId,
withCommunity: false,
includePromotedContent: false,
withVoice: false
},
features: {
creator_subscriptions_tweet_preview_api_enabled: true,
premium_content_api_read_enabled: false,
communities_web_enable_tweet_community_results_fetch: true,
c9s_tweet_anatomy_moderator_badge_enabled: true,
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
responsive_web_grok_analyze_post_followups_enabled: false,
responsive_web_jetfuel_frame: false,
responsive_web_grok_share_attachment_enabled: true,
articles_preview_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: true,
tweet_awards_web_tipping_enabled: false,
responsive_web_grok_show_grok_translated_post: false,
responsive_web_grok_analysis_button_from_backend: false,
creator_subscriptions_quote_tweet_preview_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
profile_label_improvements_pcf_label_in_post_enabled: true,
rweb_tipjar_consumption_enabled: true,
verified_phone_label_enabled: false,
responsive_web_grok_image_annotation_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_enhance_cards_enabled: false
},
fieldToggles: {
withArticleRichContentState: true,
withArticlePlainText: false,
withGrokAnalyze: false,
withDisallowedReplyControls: false
}
});
}
};
var determineClickedIndex = (clickedElement, mediaItems) => {
try {
const elementUrl = resolveClickedElementUrl(clickedElement);
if (!elementUrl) return 0;
const normalizedElementUrl = normalizeMediaUrl(elementUrl);
if (!normalizedElementUrl) return 0;
const index = mediaItems.findIndex((item) => {
if (!item) return false;
return getNormalizedMediaCandidates(item).includes(normalizedElementUrl);
});
if (index !== -1) return index;
return 0;
} catch (error) {
return 0;
}
};
var resolveClickedElementUrl = (clickedElement) => {
const mediaElement = findMediaElementInDOM(clickedElement);
const elementUrl = mediaElement ? extractMediaUrlFromElement(mediaElement) : null;
if (elementUrl) return elementUrl;
return extractBackgroundImageUrl(mediaElement ?? clickedElement, 3);
};
var extractBackgroundImageUrl = (element, maxAncestorHops) => {
if (!element) return null;
let current = element;
for (let hops = 0; hops <= maxAncestorHops && current; hops += 1) {
const url = extractUrlFromCssValue((globalThis.getComputedStyle?.(current))?.backgroundImage ?? "");
if (url) return url;
current = current.parentElement;
}
return null;
};
var extractUrlFromCssValue = (value) => {
if (!value || value === "none") return null;
return value.match(/url\((?:"|')?(.*?)(?:"|')?\)/i)?.[1]?.trim() || null;
};
var getNormalizedMediaCandidates = (item) => {
const candidates = [
item.url,
item.originalUrl,
item.thumbnailUrl
];
const apiData = item.metadata?.apiData;
if (apiData) candidates.push(getStringValue(apiData, "download_url"), getStringValue(apiData, "preview_url"), getStringValue(apiData, "expanded_url"), getStringValue(apiData, "short_expanded_url"), getStringValue(apiData, "short_tweet_url"));
const normalized = candidates.map((candidate) => candidate ? normalizeMediaUrl(candidate) : null).filter((candidate) => !!candidate);
return Array.from(new Set(normalized));
};
var getStringValue = (record, key) => {
const value = record[key];
return typeof value === "string" && value.trim() ? value : null;
};
var TwitterAPIExtractor = class {
async extract(tweetInfo, clickedElement, _options, extractionId) {
const startedAt = getTimestamp();
try {
const apiMedias = await TwitterAPI.getTweetMedias(tweetInfo.tweetId);
if (!apiMedias || apiMedias.length === 0) return createFailureResult("No media found in API response", startedAt, "twitter-api", "api-extraction-failed");
const mediaItems = await convertAPIMediaToMediaInfo(apiMedias, tweetInfo, extractTweetTextHTMLFromClickedElement(clickedElement));
return {
success: true,
mediaItems,
clickedIndex: determineClickedIndex(clickedElement, mediaItems),
metadata: {
extractedAt: Date.now(),
sourceType: "twitter-api",
strategy: "api-extraction",
totalProcessingTime: getElapsedTime(startedAt),
apiMediaCount: apiMedias.length
},
tweetInfo
};
} catch (error) {
return createFailureResult(normalizeErrorMessage(error), startedAt, "twitter-api", "api-extraction-failed");
}
}
};
var ExtractionError = class extends Error {
constructor(code, message, originalError) {
super(message);
this.code = code;
this.originalError = originalError;
this.name = "ExtractionError";
}
};
var ErrorCode = {
NONE: "NONE",
CANCELLED: "CANCELLED",
NETWORK: "NETWORK",
TIMEOUT: "TIMEOUT",
EMPTY_INPUT: "EMPTY_INPUT",
ALL_FAILED: "ALL_FAILED",
PARTIAL_FAILED: "PARTIAL_FAILED",
UNKNOWN: "UNKNOWN",
ELEMENT_NOT_FOUND: "ELEMENT_NOT_FOUND",
INVALID_ELEMENT: "INVALID_ELEMENT",
NO_MEDIA_FOUND: "NO_MEDIA_FOUND",
INVALID_URL: "INVALID_URL",
PERMISSION_DENIED: "PERMISSION_DENIED"
};
function createId() {
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID().replaceAll("-", "");
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
}
function createPrefixedId(prefix, separator = "_") {
return `${prefix}${separator}${createId()}`;
}
var generateExtractionId = () => createPrefixedId("simp");
var createErrorResult = (error) => {
const errorMessage = normalizeErrorMessage(error);
return {
success: false,
mediaItems: [],
clickedIndex: 0,
metadata: {
extractedAt: Date.now(),
sourceType: "extraction-failed",
strategy: "media-extraction",
error: errorMessage
},
tweetInfo: null,
errors: [new ExtractionError(ErrorCode.NO_MEDIA_FOUND, errorMessage)]
};
};
var createApiErrorResult = (apiResult, tweetInfo) => {
const base = createErrorResult(apiResult.metadata?.error ?? apiResult.errors?.[0]?.message ?? "API extraction failed");
return {
...base,
clickedIndex: apiResult.clickedIndex ?? 0,
metadata: {
...base.metadata,
...apiResult.metadata ?? {},
strategy: "api-extraction",
sourceType: "extraction-failed"
},
tweetInfo: mergeTweetInfoMetadata(tweetInfo, apiResult.tweetInfo)
};
};
var mergeTweetInfoMetadata = (base, override) => {
if (!base) return override ?? null;
if (!override) return base;
return {
...base,
...override,
metadata: {
...base.metadata ?? {},
...override.metadata ?? {}
}
};
};
var finalizeResult = (result) => {
if (!result.success) return result;
const uniqueItems = removeDuplicateMediaItems(result.mediaItems);
if (uniqueItems.length === 0) return {
...result,
mediaItems: [],
clickedIndex: 0
};
const adjustedIndex = adjustClickedIndexAfterDeduplication(result.mediaItems, uniqueItems, result.clickedIndex ?? 0);
return {
...result,
mediaItems: uniqueItems,
clickedIndex: adjustedIndex
};
};
var MediaExtractionService = class {
tweetInfoExtractor;
apiExtractor;
domFallbackExtractor;
constructor() {
this.tweetInfoExtractor = new TweetInfoExtractor();
this.apiExtractor = new TwitterAPIExtractor();
this.domFallbackExtractor = new DOMFallbackExtractor();
}
async extractFromClickedElement(element, options = {}) {
const extractionId = generateExtractionId();
try {
const tweetInfo = await this.tweetInfoExtractor.extract(element);
if (!tweetInfo?.tweetId) return createErrorResult("No tweet information found");
const apiResult = await this.apiExtractor.extract(tweetInfo, element, options, extractionId);
if (apiResult.success && apiResult.mediaItems.length > 0) return finalizeResult({
...apiResult,
tweetInfo: mergeTweetInfoMetadata(tweetInfo, apiResult.tweetInfo)
});
const domResult = await this.domFallbackExtractor.extract(tweetInfo, element, options, extractionId);
if (domResult.success && domResult.mediaItems.length > 0) return finalizeResult({
...domResult,
tweetInfo: mergeTweetInfoMetadata(tweetInfo, domResult.tweetInfo)
});
return createApiErrorResult(apiResult, tweetInfo);
} catch (error) {
return createErrorResult(error);
}
}
async extractAllFromContainer(container, options = {}) {
try {
const firstMedia = container.querySelector(TWITTER_MEDIA_SELECTOR);
if (!firstMedia || !(firstMedia instanceof HTMLElement)) return createErrorResult("No media found in container");
return this.extractFromClickedElement(firstMedia, options);
} catch (error) {
return createErrorResult(error);
}
}
};
var _instance = null;
var MediaService = class MediaService {
mediaExtraction = null;
prefetchManager = new PrefetchManager(20);
didCleanup = false;
_initialized = false;
async initialize() {
if (this._initialized) return;
this.mediaExtraction = new MediaExtractionService();
this._initialized = true;
}
destroy() {
this.cleanupOnce();
this._initialized = false;
}
isInitialized() {
return this._initialized;
}
static getInstance() {
if (!_instance) _instance = new MediaService();
return _instance;
}
cleanupOnce() {
if (this.didCleanup) return;
this.didCleanup = true;
this.prefetchManager.destroy();
}
async extractFromClickedElement(element, options = {}) {
if (!this.mediaExtraction) throw new Error("Media Extraction not initialized");
const result = await this.mediaExtraction.extractFromClickedElement(element, options);
if (result.success && result.mediaItems.length > 0) {
const items = result.mediaItems;
const clickedIndex = clampIndex(result.clickedIndex ?? 0, items.length);
const scheduled =  new Set();
const clickedItem = items[clickedIndex];
if (clickedItem) {
scheduled.add(clickedItem.url);
this.prefetchMedia(clickedItem, "immediate");
}
items.forEach((item, index) => {
if (!item) return;
if (index === clickedIndex) return;
if (scheduled.has(item.url)) return;
scheduled.add(item.url);
this.prefetchMedia(item, "idle");
});
}
return result;
}
async extractAllFromContainer(container, options = {}) {
if (!this.mediaExtraction) throw new Error("Media Extraction not initialized");
return this.mediaExtraction.extractAllFromContainer(container, options);
}
async prefetchMedia(media, schedule = "idle") {
return this.prefetchManager.prefetch(media, schedule);
}
getCachedMedia(url) {
return this.prefetchManager.get(url);
}
cancelAllPrefetch() {
this.prefetchManager.cancelAll();
}
clearPrefetchCache() {
this.prefetchManager.clear();
}
async cleanup() {
this.cancelAllPrefetch();
this.clearPrefetchCache();
this.cleanupOnce();
}
};
var APP_SETTINGS_STORAGE_KEY = "xeg-app-settings";
var DEFAULT_SETTINGS = {
gallery: {
autoScrollSpeed: 5,
infiniteScroll: true,
preloadCount: 3,
imageFitMode: "fitWidth",
theme: "auto",
animations: true,
enableKeyboardNav: true,
videoVolume: 1,
videoMuted: false
},
toolbar: { autoHideDelay: 3e3 },
download: {
filenamePattern: "original",
imageQuality: "original",
maxConcurrentDownloads: 3,
autoZip: false,
folderStructure: "flat"
},
accessibility: {
reduceMotion: false,
screenReaderSupport: true,
focusIndicators: true
},
features: {
gallery: true,
settings: true,
download: true,
mediaExtraction: true,
accessibility: true
},
version: "1.9.2",
lastModified: 0
};
function createDefaultSettings(timestamp = Date.now()) {
const settings = globalThis.structuredClone(DEFAULT_SETTINGS);
settings.lastModified = timestamp;
return settings;
}
var _settings = null;
function registerSettings(s) {
_settings = s;
}
function tryGetSettings() {
return _settings;
}
function registerSettingsManager(settings) {
registerSettings(settings);
}
function tryGetSettingsManager() {
return tryGetSettings();
}
function requireSettingsService() {
const service = tryGetSettings();
if (!service) throw new Error("SettingsService not registered.");
return service;
}
function getTypedSettingOr(path, fallback) {
const value = requireSettingsService().get(path);
return value === void 0 ? fallback : value;
}
function setTypedSetting(path, value) {
return requireSettingsService().set(path, value);
}
var THEME_DOM_ATTRIBUTE = "data-theme";
function syncThemeAttributes(theme, options = {}) {
if (typeof document === "undefined") return;
const { scopes, includeDocumentRoot = false } = options;
if (includeDocumentRoot && document.documentElement) document.documentElement.setAttribute(THEME_DOM_ATTRIBUTE, theme);
const targets = scopes ?? document.querySelectorAll(".xeg-theme-scope");
for (const target of Array.from(targets)) if (target instanceof HTMLElement) target.setAttribute(THEME_DOM_ATTRIBUTE, theme);
}
var _eventManagerInstance = null;
var EventManager = class EventManager {
_initialized = false;
isDestroyed = false;
listeners =  new Map();
ownedListenerContexts =  new Map();
constructor() {}
static getInstance() {
if (!_eventManagerInstance) _eventManagerInstance = new EventManager();
return _eventManagerInstance;
}
async initialize() {
if (this._initialized) return;
this.isDestroyed = false;
this._initialized = true;
}
destroy() {
if (!this._initialized) return;
this.cleanup();
this._initialized = false;
}
isInitialized() {
return this._initialized;
}
addEventListener(element, type, listener, options) {
if (this.isDestroyed) {
;
return null;
}
const { context, ...listenerOptions } = options ?? {};
if (!element || typeof element.addEventListener !== "function") return null;
try {
element.addEventListener(type, listener, listenerOptions);
const id = context ? `${context}:${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` : crypto.randomUUID().replaceAll("-", "");
const ctx = {
id,
element,
type,
listener,
options: listenerOptions,
context
};
this.listeners.set(id, ctx);
this.ownedListenerContexts.set(id, context);
return id;
} catch (error) {
return null;
}
}
removeListener(id) {
if (!this.ownedListenerContexts.has(id)) return false;
this.ownedListenerContexts.delete(id);
return this.removeListenerById(id);
}
removeByContext(context) {
const toRemove = [];
for (const [id, ctx] of this.ownedListenerContexts) if (ctx === context) toRemove.push(id);
let count = 0;
for (const id of toRemove) {
this.ownedListenerContexts.delete(id);
if (this.removeListenerById(id)) count++;
}
return count;
}
getIsDestroyed() {
return this.isDestroyed;
}
getListenerStatus() {
return {
total: 0,
byContext: {},
byType: {}
};
}
cleanup() {
if (this.isDestroyed) return;
const ids = Array.from(this.ownedListenerContexts.keys());
this.ownedListenerContexts.clear();
for (const id of ids) try {
this.removeListenerById(id);
} catch {}
this.isDestroyed = true;
}
removeListenerById(id) {
const ctx = this.listeners.get(id);
if (!ctx) return false;
try {
ctx.element.removeEventListener(ctx.type, ctx.listener, ctx.options);
this.listeners.delete(id);
return true;
} catch (error) {
return false;
}
}
};
var MAX_RECURSION_DEPTH = 5;
var VALID_THEME_SETTINGS = [
"light",
"dark",
"auto"
];
function isThemeSetting(value) {
return typeof value === "string" && VALID_THEME_SETTINGS.includes(value);
}
var _themeInstance = null;
var ThemeService = class ThemeService {
_initialized = false;
storage = getPersistentStorage();
mediaQueryList = null;
mediaQueryListener = null;
domEventsController = null;
currentTheme = "light";
themeSetting = "auto";
listeners =  new Set();
boundSettingsService = null;
observer = null;
observedThemeScopes =  new WeakSet();
recursionDepth = 0;
static getInstance() {
if (!_themeInstance) _themeInstance = new ThemeService();
return _themeInstance;
}
constructor() {
this.mediaQueryList = this.createMediaQueryList();
}
async initialize() {
if (this._initialized) return;
if (!this.boundSettingsService) {
const settingsService = tryGetSettings();
if (settingsService) this.bindSettingsService(settingsService);
else await this.restoreThemeSettingFromStorage();
}
this.initializeThemeScopeObservation();
this.initializeSystemDetection();
this.applyCurrentTheme(true);
this._initialized = true;
}
destroy() {
this.cleanup();
this._initialized = false;
}
isInitialized() {
return this._initialized;
}
bindSettingsService(settingsService) {
if (!settingsService || this.boundSettingsService === settingsService) return;
this.boundSettingsService = settingsService;
const settingsTheme = settingsService.get?.("gallery.theme");
if (isThemeSetting(settingsTheme) && settingsTheme !== this.themeSetting) {
this.themeSetting = settingsTheme;
this.applyCurrentTheme(true);
}
}
setTheme(setting, options) {
const normalized = isThemeSetting(setting) ? setting : "light";
this.themeSetting = normalized;
if (options?.persist !== false && this.boundSettingsService?.set) {
const result = this.boundSettingsService.set("gallery.theme", this.themeSetting);
if (result instanceof Promise) result.catch((error) => {});
}
if (!this.applyCurrentTheme(options?.force)) this.notifyListeners();
}
getEffectiveTheme() {
if (this.themeSetting === "auto") return this.mediaQueryList?.matches ? "dark" : "light";
return this.themeSetting;
}
getCurrentTheme() {
return this.themeSetting;
}
isDarkMode() {
return this.getEffectiveTheme() === "dark";
}
onThemeChange(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
applyThemeToScopes(scopes) {
const newScopes = [];
for (const scope of scopes) if (!this.observedThemeScopes.has(scope)) {
this.observedThemeScopes.add(scope);
newScopes.push(scope);
}
if (newScopes.length > 0) syncThemeAttributes(this.currentTheme, { scopes: newScopes });
}
createMediaQueryList() {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return null;
return window.matchMedia("(prefers-color-scheme: dark)");
}
async restoreThemeSettingFromStorage() {
const saved = await this.loadThemeAsync();
if (saved && saved !== this.themeSetting) {
this.themeSetting = saved;
this.applyCurrentTheme(true);
}
}
initializeThemeScopeObservation() {
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
this.applyThemeToScopes(Array.from(document.querySelectorAll(".xeg-theme-scope")));
this.observer?.disconnect();
this.observer = new MutationObserver((mutations) => {
for (const mutation of mutations) mutation.addedNodes.forEach((node) => {
if (!(node instanceof Element)) return;
const scopes = [];
if (node.classList.contains("xeg-theme-scope")) scopes.push(node);
node.querySelectorAll(".xeg-theme-scope").forEach((scope) => {
scopes.push(scope);
});
if (scopes.length > 0) this.applyThemeToScopes(scopes);
});
});
if (document.body) {
this.observer.observe(document.body, {
childList: true,
subtree: true
});
return;
}
}
cleanup() {
this.boundSettingsService = null;
this.listeners.clear();
this.observedThemeScopes =  new WeakSet();
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
if (this.domEventsController) {
this.domEventsController.abort();
this.domEventsController = null;
}
this.mediaQueryListener = null;
this.mediaQueryList = null;
}
initializeSystemDetection() {
if (!this.mediaQueryList) this.mediaQueryList = this.createMediaQueryList();
if (!this.mediaQueryList || this.mediaQueryListener) return;
if (!this.domEventsController || this.domEventsController.signal.aborted) this.domEventsController = new AbortController();
this.mediaQueryListener = () => {
if (this.themeSetting === "auto") this.applyCurrentTheme();
};
const listener = this.mediaQueryListener;
const eventListener = (event) => {
listener(event);
};
EventManager.getInstance().addEventListener(this.mediaQueryList, "change", eventListener, {
signal: this.domEventsController.signal,
context: "theme-service"
});
}
applyCurrentTheme(force = false) {
const effective = this.getEffectiveTheme();
if (force || this.currentTheme !== effective) {
this.currentTheme = effective;
syncThemeAttributes(this.currentTheme);
this.notifyListeners();
return true;
}
return false;
}
notifyListeners() {
if (this.recursionDepth >= MAX_RECURSION_DEPTH) {
this.recursionDepth = 0;
return;
}
this.recursionDepth++;
try {
this.listeners.forEach((listener) => listener(this.currentTheme, this.themeSetting));
} finally {
this.recursionDepth--;
}
}
async loadThemeAsync() {
try {
return (await this.storage.get("xeg-app-settings"))?.gallery?.theme ?? null;
} catch {
return null;
}
}
};
async function initializeCoreBaseServices() {
try {
const services = [
ThemeService.getInstance(),
LanguageService.getInstance(),
MediaService.getInstance()
];
for (const service of services) if (service?.initialize) await service.initialize();
} catch (error) {
throw new Error("[base-services] initialization failed", { cause: error instanceof Error ? error : new Error(String(error)) });
}
}
function sanitize(name) {
return name.replace(/[<>:"/\\|?*]/g, "_").replace(/^[\s.]+|[\s.]+$/g, "").slice(0, 255) || "media";
}
function resolveNowMs$1(nowMs) {
return Number.isFinite(nowMs) ? nowMs : 0;
}
function getExtension(url) {
try {
const path = url.split("?")[0];
if (!path) return "jpg";
const ext = path.split(".").pop();
if (ext && /^(jpg|jpeg|png|gif|webp|mp4|mov|avi)$/i.test(ext)) return ext.toLowerCase();
} catch {}
return "jpg";
}
function getIndexFromMediaId(mediaId) {
if (!mediaId) return null;
const match = mediaId.match(/_media_(\d+)$/) || mediaId.match(/_(\d+)$/);
if (match) {
const idx = safeParseInt(match[1], 10);
return mediaId.includes("_media_") ? (idx + 1).toString() : match[1] ?? null;
}
return null;
}
function normalizeIndex(index) {
if (index === void 0 || index === null) return "1";
const num = typeof index === "string" ? safeParseInt(index, 10) : index;
return Number.isNaN(num) || num < 1 ? "1" : num.toString();
}
function resolveMetadata(media, fallbackUsername) {
let username = null;
let tweetId = null;
if (media.sourceLocation === "quoted" && media.quotedUsername && media.quotedTweetId) {
username = media.quotedUsername;
tweetId = media.quotedTweetId;
} else {
tweetId = media.tweetId ?? null;
if (media.tweetUsername && media.tweetUsername !== "unknown") username = media.tweetUsername;
else {
const url = ("originalUrl" in media ? media.originalUrl : null) || media.url;
if (typeof url === "string") username = extractUsernameFromUrl(url, { strictHost: true });
}
}
if (!username && fallbackUsername) username = fallbackUsername;
return {
username,
tweetId
};
}
function generateMediaFilename(media, options = {}) {
try {
if (media.filename) return sanitize(media.filename);
const nowMs = resolveNowMs$1(options.nowMs);
const extension = options.extension ?? getExtension(media.originalUrl ?? media.url);
const index = getIndexFromMediaId(media.id) ?? normalizeIndex(options.index);
const { username, tweetId } = resolveMetadata(media, options.fallbackUsername);
if (username && tweetId) return sanitize(`${username}_${tweetId}_${index}.${extension}`);
if (tweetId && /^\d+$/.test(tweetId)) return sanitize(`tweet_${tweetId}_${index}.${extension}`);
return sanitize(`${options.fallbackPrefix ?? "media"}_${nowMs}_${index}.${extension}`);
} catch {
return `media_${resolveNowMs$1(options.nowMs)}.${options.extension || "jpg"}`;
}
}
function generateZipFilename(mediaItems, options = {}) {
try {
const firstItem = mediaItems[0];
if (firstItem) {
const { username, tweetId } = resolveMetadata(firstItem);
if (username && tweetId) return sanitize(`${username}_${tweetId}.zip`);
}
return sanitize(`${options.fallbackPrefix ?? "xcom_gallery"}_${resolveNowMs$1(options.nowMs)}.zip`);
} catch {
return `download_${resolveNowMs$1(options.nowMs)}.zip`;
}
}
function generateDesiredName(media, nowMs) {
return nowMs === void 0 ? generateMediaFilename(media) : generateMediaFilename(media, { nowMs });
}
function generateZipName(items, nowMs) {
return nowMs === void 0 ? generateZipFilename(items) : generateZipFilename(items, { nowMs });
}
function planBulkDownload(input) {
return {
items: input.mediaItems.map((media) => ({
url: media.url,
desiredName: generateDesiredName(media, input.nowMs),
blob: input.prefetchedBlobs?.get(media.url)
})),
zipFilename: input.zipFilename ?? generateZipName(input.mediaItems, input.nowMs)
};
}
function planZipSave(method) {
return method === "gm_download" ? "gm_download" : "none";
}
var GM_API_CHECKS = {
getValue: (gm) => typeof gm.getValue === "function",
setValue: (gm) => typeof gm.setValue === "function",
download: (gm) => typeof gm.download === "function",
notification: (gm) => typeof gm.notification === "function",
deleteValue: (gm) => typeof gm.deleteValue === "function",
listValues: (gm) => typeof gm.listValues === "function",
cookie: (gm) => typeof gm.cookie?.list === "function"
};
function isGMAPIAvailable(apiName) {
const checker = GM_API_CHECKS[apiName];
try {
return checker(getResolvedGMAPIsCached());
} catch {
return false;
}
}
function asGMDownloadFunction(value) {
return typeof value === "function" ? value : void 0;
}
function detectDownloadCapability() {
const gmDownload = asGMDownloadFunction(resolveGMDownload());
const hasGMDownload = !!gmDownload && isGMAPIAvailable("download");
return {
hasGMDownload,
method: hasGMDownload ? "gm_download" : "none",
gmDownload: hasGMDownload ? gmDownload : void 0
};
}
var DOWNLOAD_TIMEOUT_MS = 3e4;
var DOWNLOAD_TIMEOUT_MESSAGE = "Download timeout";
var reportProgress$1 = (onProgress, phase, percentage, filename) => {
if (!onProgress) return;
onProgress({
phase,
current: 1,
total: 1,
percentage,
filename
});
};
var createAbortResult = () => ({
success: false,
error: "Download cancelled by user"
});
async function downloadSingleFile(media, options = {}, capability) {
if (options.signal?.aborted) return createAbortResult();
const filename = generateMediaFilename(media, { nowMs: Date.now() });
const gmDownload = (capability ?? detectDownloadCapability()).gmDownload;
if (!gmDownload) return {
success: false,
error: "No download method available"
};
reportProgress$1(options.onProgress, "preparing", 0, filename);
let url = media.url;
let isBlobUrl = false;
const blob = options.blob;
if (blob) {
url = URL.createObjectURL(blob);
isBlobUrl = true;
}
return new Promise((resolve) => {
let timer;
let settled = false;
const cleanup = () => {
if (isBlobUrl) URL.revokeObjectURL(url);
if (timer) globalTimerManager.clearTimeout(timer);
};
const settle = (result) => {
if (settled) return;
settled = true;
if (result.success) reportProgress$1(options.onProgress, "complete", 100, filename);
cleanup();
resolve(result);
};
timer = globalTimerManager.setTimeout(() => {
settle({
success: false,
error: DOWNLOAD_TIMEOUT_MESSAGE
});
}, DOWNLOAD_TIMEOUT_MS);
try {
gmDownload({
url,
name: filename,
onload: () => settle({
success: true,
filename
}),
onerror: (error) => {
settle({
success: false,
error: normalizeErrorMessage(error)
});
},
ontimeout: () => settle({
success: false,
error: DOWNLOAD_TIMEOUT_MESSAGE
}),
onprogress: (progress) => {
if (settled || !options.onProgress || progress.total <= 0) return;
const pct = Math.min(100, Math.max(0, Math.round(progress.loaded / progress.total * 100)));
reportProgress$1(options.onProgress, "downloading", pct, filename);
}
});
} catch (error) {
settle({
success: false,
error: normalizeErrorMessage(error)
});
}
});
}
var textEncoder = new TextEncoder();
var crc32Table = null;
function ensureCRC32Table() {
if (crc32Table) return crc32Table;
const table = new Uint32Array(256);
const polynomial = 3988292384;
for (let i = 0; i < 256; i++) {
let crc = i;
for (let j = 0; j < 8; j++) crc = crc & 1 ? crc >>> 1 ^ polynomial : crc >>> 1;
table[i] = crc >>> 0;
}
crc32Table = table;
return table;
}
function encodeUtf8(value) {
return textEncoder.encode(value);
}
function calculateCRC32(data) {
const table = ensureCRC32Table();
let crc = 4294967295;
for (let i = 0; i < data.length; i++) crc = crc >>> 8 ^ table[(crc ^ data[i]) & 255];
return (crc ^ 4294967295) >>> 0;
}
function writeUint16LE(value) {
const bytes = new Uint8Array(2);
bytes[0] = value & 255;
bytes[1] = value >>> 8 & 255;
return bytes;
}
function writeUint32LE(value) {
const bytes = new Uint8Array(4);
bytes[0] = value & 255;
bytes[1] = value >>> 8 & 255;
bytes[2] = value >>> 16 & 255;
bytes[3] = value >>> 24 & 255;
return bytes;
}
var ZIP_CONST = {
MAX_UINT16: 65535,
MAX_UINT32: 4294967295,
ZIP32_ERROR: "Zip32 limit exceeded",
SIG_LOCAL_HEADER: new Uint8Array([
80,
75,
3,
4
]),
SIG_CENTRAL_DIR: new Uint8Array([
80,
75,
1,
2
]),
SIG_END_CENTRAL_DIR: new Uint8Array([
80,
75,
5,
6
]),
UTF8_FLAG: 2048,
VERSION: 20
};
function assertZip32(condition, message) {
if (condition) return;
throw new Error(ZIP_CONST.ZIP32_ERROR);
}
var concat = (arrays) => {
let len = 0;
for (const array of arrays) len += array.length;
const result = new Uint8Array(len);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
};
var StreamingZipWriter = class {
chunks = [];
entries = [];
currentOffset = 0;
addFile(filename, data) {
assertZip32(this.entries.length < ZIP_CONST.MAX_UINT16 - 1, `too many entries (count=${this.entries.length + 1})`);
assertZip32(data.length < ZIP_CONST.MAX_UINT32, `file too large (size=${data.length})`);
assertZip32(this.currentOffset < ZIP_CONST.MAX_UINT32, `offset overflow (offset=${this.currentOffset})`);
const filenameBytes = encodeUtf8(filename);
const crc32 = calculateCRC32(data);
const localHeader = concat([
ZIP_CONST.SIG_LOCAL_HEADER,
writeUint16LE(ZIP_CONST.VERSION),
writeUint16LE(ZIP_CONST.UTF8_FLAG),
writeUint16LE(0),
writeUint16LE(0),
writeUint16LE(0),
writeUint32LE(crc32),
writeUint32LE(data.length),
writeUint32LE(data.length),
writeUint16LE(filenameBytes.length),
writeUint16LE(0),
filenameBytes
]);
assertZip32(this.currentOffset + localHeader.length + data.length < ZIP_CONST.MAX_UINT32, `archive too large (offset=${this.currentOffset}, add=${localHeader.length + data.length})`);
this.chunks.push(localHeader, data);
this.entries.push({
filename,
data,
offset: this.currentOffset,
crc32
});
this.currentOffset += localHeader.length + data.length;
}
finalize() {
assertZip32(this.entries.length < ZIP_CONST.MAX_UINT16, `too many entries (count=${this.entries.length})`);
const centralDirStart = this.currentOffset;
assertZip32(centralDirStart < ZIP_CONST.MAX_UINT32, `central directory offset overflow (${centralDirStart})`);
const centralDirChunks = [];
for (const entry of this.entries) {
const filenameBytes = encodeUtf8(entry.filename);
assertZip32(entry.offset < ZIP_CONST.MAX_UINT32, `entry offset overflow (${entry.offset})`);
assertZip32(entry.data.length < ZIP_CONST.MAX_UINT32, `entry too large (size=${entry.data.length})`);
centralDirChunks.push(concat([
ZIP_CONST.SIG_CENTRAL_DIR,
writeUint16LE(ZIP_CONST.VERSION),
writeUint16LE(ZIP_CONST.VERSION),
writeUint16LE(ZIP_CONST.UTF8_FLAG),
writeUint16LE(0),
writeUint16LE(0),
writeUint16LE(0),
writeUint32LE(entry.crc32),
writeUint32LE(entry.data.length),
writeUint32LE(entry.data.length),
writeUint16LE(filenameBytes.length),
writeUint16LE(0),
writeUint16LE(0),
writeUint16LE(0),
writeUint16LE(0),
writeUint32LE(0),
writeUint32LE(entry.offset),
filenameBytes
]));
}
const centralDir = concat(centralDirChunks);
assertZip32(centralDir.length < ZIP_CONST.MAX_UINT32, `central directory too large (size=${centralDir.length})`);
const endOfCentralDir = concat([
ZIP_CONST.SIG_END_CENTRAL_DIR,
writeUint16LE(0),
writeUint16LE(0),
writeUint16LE(this.entries.length),
writeUint16LE(this.entries.length),
writeUint32LE(centralDir.length),
writeUint32LE(centralDirStart),
writeUint16LE(0)
]);
return concat([
...this.chunks,
centralDir,
endOfCentralDir
]);
}
};
var HttpStatusError = class extends Error {
name = "HttpStatusError";
constructor(status) {
super(`HTTP error: ${status}`);
this.status = status;
}
};
var isRetryableStatus = (status) => status === 0 || status === 408 || status === 425 || status === 429 || status >= 500 && status < 600;
var getStatusFromError = (error) => {
if (!error || typeof error !== "object" || !("status" in error)) return null;
const statusValue = error.status;
return typeof statusValue === "number" ? statusValue : null;
};
async function fetchArrayBufferWithRetry(url, retries, signal, backoffBaseMs = 200) {
if (signal?.aborted) throw getUserCancelledAbortErrorFromSignal(signal);
const httpService = HttpRequestService.getInstance();
const result = await withRetry(async () => {
if (signal?.aborted) throw getUserCancelledAbortErrorFromSignal(signal);
const response = await httpService.get(url, {
responseType: "arraybuffer",
timeout: 3e4,
...signal ? { signal } : {}
});
if (!response.ok) throw new HttpStatusError(response.status);
return new Uint8Array(response.data);
}, {
maxAttempts: Math.max(1, retries + 1),
baseDelayMs: backoffBaseMs,
...signal ? { signal } : {},
shouldRetry: (error) => {
if (isAbortError(error)) return false;
const status = getStatusFromError(error);
if (status === null) return true;
return isRetryableStatus(status);
}
});
if (result.success) return result.data;
if (signal?.aborted) throw getUserCancelledAbortErrorFromSignal(signal);
throw result.error;
}
var ensureUniqueFilenameFactory = () => {
const usedNames =  new Set();
const baseCounts =  new Map();
return (desired) => {
if (!usedNames.has(desired)) {
usedNames.add(desired);
baseCounts.set(desired, 0);
return desired;
}
const lastDot = desired.lastIndexOf(".");
const name = lastDot > 0 ? desired.slice(0, lastDot) : desired;
const ext = lastDot > 0 ? desired.slice(lastDot) : "";
const baseKey = desired;
let count = baseCounts.get(baseKey) ?? 0;
let candidate = "";
do {
count += 1;
candidate = `${name}-${count}${ext}`;
} while (usedNames.has(candidate));
baseCounts.set(baseKey, count);
usedNames.add(candidate);
return candidate;
};
};
var MAX_CONCURRENCY = 8;
var MIN_CONCURRENCY = 1;
var DEFAULT_CONCURRENCY = 4;
var DEFAULT_RETRIES = 3;
var clampConcurrency = (value) => {
return Math.min(MAX_CONCURRENCY, Math.max(MIN_CONCURRENCY, value ?? DEFAULT_CONCURRENCY));
};
var clampRetries = (value) => Math.max(0, value ?? DEFAULT_RETRIES);
var calculatePercentage = (current, total) => {
if (total <= 0) return 0;
return Math.min(100, Math.max(0, Math.round(current / total * 100)));
};
var reportProgress = (onProgress, payload) => {
if (!onProgress) return;
const percentage = payload.percentage ?? calculatePercentage(payload.current, payload.total);
onProgress({
...payload,
percentage
});
};
var throwIfAborted = (signal) => {
if (signal?.aborted) throw getUserCancelledAbortErrorFromSignal(signal);
};
async function downloadAsZip(items, options = {}) {
const writer = new StreamingZipWriter();
const concurrency = clampConcurrency(options.concurrency);
const retries = clampRetries(options.retries);
const abortSignal = options.signal;
const onProgress = options.onProgress;
throwIfAborted(abortSignal);
const total = items.length;
let processed = 0;
let successful = 0;
const failures = [];
const ensureUniqueFilename = ensureUniqueFilenameFactory();
const assignedFilenames = items.map((item) => ensureUniqueFilename(item.desiredName));
let currentIndex = 0;
const runNext = async () => {
while (currentIndex < total) {
throwIfAborted(abortSignal);
const index = currentIndex++;
const item = items[index];
if (!item) continue;
const filename = assignedFilenames[index] ?? item.desiredName;
try {
let data;
if (item.blob) {
const blob = item.blob instanceof Promise ? await item.blob : item.blob;
throwIfAborted(abortSignal);
data = new Uint8Array(await blob.arrayBuffer());
} else data = await fetchArrayBufferWithRetry(item.url, retries, abortSignal, 200);
throwIfAborted(abortSignal);
writer.addFile(filename, data);
successful++;
} catch (error) {
throwIfAborted(abortSignal);
failures.push({
url: item.url,
error: normalizeErrorMessage(error)
});
} finally {
processed++;
reportProgress(onProgress, {
phase: "downloading",
current: processed,
total,
filename
});
}
}
};
const workers = Array.from({ length: Math.min(concurrency, total) }, () => runNext());
await Promise.all(workers);
reportProgress(onProgress, {
phase: "complete",
current: processed,
total
});
const zipBytes = writer.finalize();
return {
filesSuccessful: successful,
failures,
zipData: zipBytes
};
}
var _downloadInstance = null;
var DownloadOrchestrator = class DownloadOrchestrator {
capability = null;
_initialized = false;
constructor() {}
static getInstance() {
if (!_downloadInstance) _downloadInstance = new DownloadOrchestrator();
return _downloadInstance;
}
async initialize() {
this._initialized = true;
}
destroy() {
this.capability = null;
this._initialized = false;
}
isInitialized() {
return this._initialized;
}
getCapability() {
return this.capability ??= detectDownloadCapability();
}
async downloadSingle(media, options = {}) {
return downloadSingleFile(media, options, this.getCapability());
}
async downloadBulk(mediaItems, options = {}) {
if (options.signal?.aborted) return {
success: false,
status: "error",
filesProcessed: 0,
filesSuccessful: 0,
error: normalizeErrorMessage(getUserCancelledAbortErrorFromSignal(options.signal)),
code: ErrorCode.CANCELLED
};
if (mediaItems.length === 0) return {
success: false,
status: "error",
filesProcessed: 0,
filesSuccessful: 0,
error: "No media to download",
code: ErrorCode.EMPTY_INPUT
};
const capability = this.getCapability();
if (capability.method === "none") return {
success: false,
status: "error",
filesProcessed: mediaItems.length,
filesSuccessful: 0,
error: "No download method",
code: ErrorCode.ALL_FAILED
};
const plan = planBulkDownload({
mediaItems,
prefetchedBlobs: options.prefetchedBlobs,
zipFilename: options.zipFilename,
nowMs: Date.now()
});
const items = plan.items;
try {
const result = await downloadAsZip(items, options);
if (result.filesSuccessful === 0) return {
success: false,
status: "error",
filesProcessed: items.length,
filesSuccessful: 0,
error: "No files downloaded",
failures: result.failures,
code: ErrorCode.ALL_FAILED
};
const zipBlob = new Blob([result.zipData], { type: "application/zip" });
const filename = plan.zipFilename;
const saveResult = await this.saveZipBlob(zipBlob, filename, options, capability);
if (!saveResult.success) return {
success: false,
status: "error",
filesProcessed: items.length,
filesSuccessful: result.filesSuccessful,
error: saveResult.error || "Failed to save ZIP file",
failures: result.failures,
code: ErrorCode.ALL_FAILED
};
return {
success: true,
status: result.filesSuccessful === items.length ? "success" : "partial",
filesProcessed: items.length,
filesSuccessful: result.filesSuccessful,
filename,
failures: result.failures,
code: ErrorCode.NONE
};
} catch (error) {
if (isAbortError(error)) return {
success: false,
status: "error",
filesProcessed: items.length,
filesSuccessful: 0,
error: normalizeErrorMessage(error),
code: ErrorCode.CANCELLED
};
return {
success: false,
status: "error",
filesProcessed: items.length,
filesSuccessful: 0,
error: normalizeErrorMessage(error),
code: ErrorCode.ALL_FAILED
};
}
}
async saveZipBlob(zipBlob, filename, options, capability) {
if (planZipSave(capability.method) === "gm_download" && capability.gmDownload) return this.saveWithGMDownload(capability.gmDownload, zipBlob, filename, options.onProgress);
return {
success: false,
error: "No download method"
};
}
async saveWithGMDownload(gmDownload, blob, filename, onprogress) {
const url = URL.createObjectURL(blob);
try {
await new Promise((resolve, reject) => {
gmDownload({
url,
name: filename,
onload: () => resolve(),
onerror: (err) => reject(err),
ontimeout: () => reject(  new Error("Timeout")),
...onprogress ? { onprogress: (progress) => {
if (progress.total <= 0) return;
const pct = Math.min(100, Math.max(0, Math.round(progress.loaded / progress.total * 100)));
onprogress({
phase: "saving",
current: progress.loaded,
total: progress.total,
percentage: pct,
filename
});
} } : {}
});
});
return { success: true };
} catch (error) {
return {
success: false,
error: normalizeErrorMessage(error)
};
} finally {
URL.revokeObjectURL(url);
}
}
};
var _renderer = null;
function registerRenderer(r) {
_renderer = r;
}
function hasRenderer() {
return _renderer !== null;
}
function getThemeService() {
return ThemeService.getInstance();
}
function getLanguageService() {
return LanguageService.getInstance();
}
function getMediaService() {
return MediaService.getInstance();
}
var _downloadOrchestrator = null;
function getDownloadOrchestrator() {
if (!_downloadOrchestrator) _downloadOrchestrator = DownloadOrchestrator.getInstance();
return _downloadOrchestrator;
}
var DEFAULT_SEVERITY = "error";
var notificationCallback = null;
function reportError(error, options) {
const severity = options.severity ?? DEFAULT_SEVERITY;
const message = normalizeErrorMessage(error);
const payload = {
c: options.context,
s: severity
};
if (options.code) payload.cd = options.code;
if (options.metadata) payload.m = options.metadata;
if (options.notify && notificationCallback) notificationCallback(message, options.context);
if (severity === "critical") console.error("[Critical Error]", message, payload);
return {
reported: true,
message,
context: options.context,
severity
};
}
function forContext(context) {
const bind = (severity) => (error, options) => reportError(error, {
...options,
context,
severity
});
return {
critical: bind("critical"),
error: bind("error"),
warn: bind("warning"),
info: bind("info")
};
}
var bootstrapErrorReporter = forContext("bootstrap");
var galleryErrorReporter = forContext("gallery");
var mediaErrorReporter = forContext("media");
var settingsErrorReporter = forContext("settings");
var sharedConfig = {
context: void 0,
registry: void 0,
effects: void 0,
done: false,
getContextId() {
return getContextId(this.context.count);
},
getNextContextId() {
return getContextId(this.context.count++);
}
};
function getContextId(count) {
const num = String(count), len = num.length - 1;
return sharedConfig.context.id + (len ? String.fromCharCode(96 + len) : "") + num;
}
function setHydrateContext(context) {
sharedConfig.context = context;
}
function nextHydrateContext() {
return {
...sharedConfig.context,
id: sharedConfig.getNextContextId(),
count: 0
};
}
var equalFn = (a, b) => a === b;
var $PROXY = Symbol("solid-proxy");
var SUPPORTS_PROXY = typeof Proxy === "function";
var signalOptions = { equals: equalFn };
var ERROR = null;
var runEffects = runQueue;
var STALE = 1;
var PENDING = 2;
var UNOWNED = {
owned: null,
cleanups: null,
context: null,
owner: null
};
var Owner = null;
var Transition = null;
var Scheduler = null;
var ExternalSourceConfig = null;
var Listener = null;
var Updates = null;
var Effects = null;
var ExecCount = 0;
function createRoot(fn, detachedOwner) {
const listener = Listener, owner = Owner, unowned = fn.length === 0, current = detachedOwner === void 0 ? owner : detachedOwner, root = unowned ? UNOWNED : {
owned: null,
cleanups: null,
context: current ? current.context : null,
owner: current
}, updateFn = unowned ? fn : () => fn(() => untrack(() => cleanNode(root)));
Owner = root;
Listener = null;
try {
return runUpdates(updateFn, true);
} finally {
Listener = listener;
Owner = owner;
}
}
function createSignal(value, options) {
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
const s = {
value,
observers: null,
observerSlots: null,
comparator: options.equals || void 0
};
const setter = (value) => {
if (typeof value === "function") if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue);
else value = value(s.value);
return writeSignal(s, value);
};
return [readSignal.bind(s), setter];
}
function createComputed(fn, value, options) {
const c = createComputation(fn, value, true, STALE);
if (Scheduler && Transition && Transition.running) Updates.push(c);
else updateComputation(c);
}
function createRenderEffect(fn, value, options) {
const c = createComputation(fn, value, false, STALE);
if (Scheduler && Transition && Transition.running) Updates.push(c);
else updateComputation(c);
}
function createEffect(fn, value, options) {
runEffects = runUserEffects;
const c = createComputation(fn, value, false, STALE), s = SuspenseContext && useContext(SuspenseContext);
if (s) c.suspense = s;
if (!options || !options.render) c.user = true;
Effects ? Effects.push(c) : updateComputation(c);
}
function createMemo(fn, value, options) {
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
const c = createComputation(fn, value, true, 0);
c.observers = null;
c.observerSlots = null;
c.comparator = options.equals || void 0;
if (Scheduler && Transition && Transition.running) {
c.tState = STALE;
Updates.push(c);
} else updateComputation(c);
return readSignal.bind(c);
}
function batch$1(fn) {
return runUpdates(fn, false);
}
function untrack(fn) {
if (!ExternalSourceConfig && Listener === null) return fn();
const listener = Listener;
Listener = null;
try {
if (ExternalSourceConfig) return ExternalSourceConfig.untrack(fn);
return fn();
} finally {
Listener = listener;
}
}
function on(deps, fn, options) {
const isArray = Array.isArray(deps);
let prevInput;
let defer = options && options.defer;
return (prevValue) => {
let input;
if (isArray) {
input = Array(deps.length);
for (let i = 0; i < deps.length; i++) input[i] = deps[i]();
} else input = deps();
if (defer) {
defer = false;
return prevValue;
}
const result = untrack(() => fn(input, prevInput, prevValue));
prevInput = input;
return result;
};
}
function onMount(fn) {
createEffect(() => untrack(fn));
}
function onCleanup(fn) {
if (Owner === null);
else if (Owner.cleanups === null) Owner.cleanups = [fn];
else Owner.cleanups.push(fn);
return fn;
}
function catchError(fn, handler) {
ERROR || (ERROR = Symbol("error"));
Owner = createComputation(void 0, void 0, true);
Owner.context = {
...Owner.context,
[ERROR]: [handler]
};
if (Transition && Transition.running) Transition.sources.add(Owner);
try {
return fn();
} catch (err) {
handleError(err);
} finally {
Owner = Owner.owner;
}
}
function startTransition(fn) {
if (Transition && Transition.running) {
fn();
return Transition.done;
}
const l = Listener;
const o = Owner;
return Promise.resolve().then(() => {
Listener = l;
Owner = o;
let t;
if (Scheduler || SuspenseContext) {
t = Transition || (Transition = {
sources:  new Set(),
effects: [],
promises:  new Set(),
disposed:  new Set(),
queue:  new Set(),
running: true
});
t.done || (t.done = new Promise((res) => t.resolve = res));
t.running = true;
}
runUpdates(fn, false);
Listener = Owner = null;
return t ? t.done : void 0;
});
}
var [transPending, setTransPending] =  createSignal(false);
function useContext(context) {
let value;
return Owner && Owner.context && (value = Owner.context[context.id]) !== void 0 ? value : context.defaultValue;
}
var SuspenseContext;
function readSignal() {
const runningTransition = Transition && Transition.running;
if (this.sources && (runningTransition ? this.tState : this.state)) if ((runningTransition ? this.tState : this.state) === STALE) updateComputation(this);
else {
const updates = Updates;
Updates = null;
runUpdates(() => lookUpstream(this), false);
Updates = updates;
}
if (Listener) {
const sSlot = this.observers ? this.observers.length : 0;
if (!Listener.sources) {
Listener.sources = [this];
Listener.sourceSlots = [sSlot];
} else {
Listener.sources.push(this);
Listener.sourceSlots.push(sSlot);
}
if (!this.observers) {
this.observers = [Listener];
this.observerSlots = [Listener.sources.length - 1];
} else {
this.observers.push(Listener);
this.observerSlots.push(Listener.sources.length - 1);
}
}
if (runningTransition && Transition.sources.has(this)) return this.tValue;
return this.value;
}
function writeSignal(node, value, isComp) {
let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
if (!node.comparator || !node.comparator(current, value)) {
if (Transition) {
const TransitionRunning = Transition.running;
if (TransitionRunning || !isComp && Transition.sources.has(node)) {
Transition.sources.add(node);
node.tValue = value;
}
if (!TransitionRunning) node.value = value;
} else node.value = value;
if (node.observers && node.observers.length) runUpdates(() => {
for (let i = 0; i < node.observers.length; i += 1) {
const o = node.observers[i];
const TransitionRunning = Transition && Transition.running;
if (TransitionRunning && Transition.disposed.has(o)) continue;
if (TransitionRunning ? !o.tState : !o.state) {
if (o.pure) Updates.push(o);
else Effects.push(o);
if (o.observers) markDownstream(o);
}
if (!TransitionRunning) o.state = STALE;
else o.tState = STALE;
}
if (Updates.length > 1e6) {
Updates = [];
throw new Error();
}
}, false);
}
return value;
}
function updateComputation(node) {
if (!node.fn) return;
cleanNode(node);
const time = ExecCount;
runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time);
if (Transition && !Transition.running && Transition.sources.has(node)) queueMicrotask(() => {
runUpdates(() => {
Transition && (Transition.running = true);
Listener = Owner = node;
runComputation(node, node.tValue, time);
Listener = Owner = null;
}, false);
});
}
function runComputation(node, value, time) {
let nextValue;
const owner = Owner, listener = Listener;
Listener = Owner = node;
try {
nextValue = node.fn(value);
} catch (err) {
if (node.pure) if (Transition && Transition.running) {
node.tState = STALE;
node.tOwned && node.tOwned.forEach(cleanNode);
node.tOwned = void 0;
} else {
node.state = STALE;
node.owned && node.owned.forEach(cleanNode);
node.owned = null;
}
node.updatedAt = time + 1;
return handleError(err);
} finally {
Listener = listener;
Owner = owner;
}
if (!node.updatedAt || node.updatedAt <= time) {
if (node.updatedAt != null && "observers" in node) writeSignal(node, nextValue, true);
else if (Transition && Transition.running && node.pure) {
if (!Transition.sources.has(node)) node.value = nextValue;
Transition.sources.add(node);
node.tValue = nextValue;
} else node.value = nextValue;
node.updatedAt = time;
}
}
function createComputation(fn, init, pure, state = STALE, options) {
const c = {
fn,
state,
updatedAt: null,
owned: null,
sources: null,
sourceSlots: null,
cleanups: null,
value: init,
owner: Owner,
context: Owner ? Owner.context : null,
pure
};
if (Transition && Transition.running) {
c.state = 0;
c.tState = state;
}
if (Owner === null);
else if (Owner !== UNOWNED) if (Transition && Transition.running && Owner.pure) if (!Owner.tOwned) Owner.tOwned = [c];
else Owner.tOwned.push(c);
else if (!Owner.owned) Owner.owned = [c];
else Owner.owned.push(c);
if (ExternalSourceConfig && c.fn) {
const sourceFn = c.fn;
const [track, trigger] = createSignal(void 0, { equals: false });
const ordinary = ExternalSourceConfig.factory(sourceFn, trigger);
onCleanup(() => ordinary.dispose());
let inTransition;
const triggerInTransition = () => startTransition(trigger).then(() => {
if (inTransition) {
inTransition.dispose();
inTransition = void 0;
}
});
c.fn = (x) => {
track();
if (Transition && Transition.running) {
if (!inTransition) inTransition = ExternalSourceConfig.factory(sourceFn, triggerInTransition);
return inTransition.track(x);
}
return ordinary.track(x);
};
}
return c;
}
function runTop(node) {
const runningTransition = Transition && Transition.running;
if ((runningTransition ? node.tState : node.state) === 0) return;
if ((runningTransition ? node.tState : node.state) === PENDING) return lookUpstream(node);
if (node.suspense && untrack(node.suspense.inFallback)) return node.suspense.effects.push(node);
const ancestors = [node];
while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) {
if (runningTransition && Transition.disposed.has(node)) return;
if (runningTransition ? node.tState : node.state) ancestors.push(node);
}
for (let i = ancestors.length - 1; i >= 0; i--) {
node = ancestors[i];
if (runningTransition) {
let top = node, prev = ancestors[i + 1];
while ((top = top.owner) && top !== prev) if (Transition.disposed.has(top)) return;
}
if ((runningTransition ? node.tState : node.state) === STALE) updateComputation(node);
else if ((runningTransition ? node.tState : node.state) === PENDING) {
const updates = Updates;
Updates = null;
runUpdates(() => lookUpstream(node, ancestors[0]), false);
Updates = updates;
}
}
}
function runUpdates(fn, init) {
if (Updates) return fn();
let wait = false;
if (!init) Updates = [];
if (Effects) wait = true;
else Effects = [];
ExecCount++;
try {
const res = fn();
completeUpdates(wait);
return res;
} catch (err) {
if (!wait) Effects = null;
Updates = null;
handleError(err);
}
}
function completeUpdates(wait) {
if (Updates) {
if (Scheduler && Transition && Transition.running) scheduleQueue(Updates);
else runQueue(Updates);
Updates = null;
}
if (wait) return;
let res;
if (Transition) {
if (!Transition.promises.size && !Transition.queue.size) {
const sources = Transition.sources;
const disposed = Transition.disposed;
Effects.push.apply(Effects, Transition.effects);
res = Transition.resolve;
for (const e of Effects) {
"tState" in e && (e.state = e.tState);
delete e.tState;
}
Transition = null;
runUpdates(() => {
for (const d of disposed) cleanNode(d);
for (const v of sources) {
v.value = v.tValue;
if (v.owned) for (let i = 0, len = v.owned.length; i < len; i++) cleanNode(v.owned[i]);
if (v.tOwned) v.owned = v.tOwned;
delete v.tValue;
delete v.tOwned;
v.tState = 0;
}
setTransPending(false);
}, false);
} else if (Transition.running) {
Transition.running = false;
Transition.effects.push.apply(Transition.effects, Effects);
Effects = null;
setTransPending(true);
return;
}
}
const e = Effects;
Effects = null;
if (e.length) runUpdates(() => runEffects(e), false);
if (res) res();
}
function runQueue(queue) {
for (let i = 0; i < queue.length; i++) runTop(queue[i]);
}
function scheduleQueue(queue) {
for (let i = 0; i < queue.length; i++) {
const item = queue[i];
const tasks = Transition.queue;
if (!tasks.has(item)) {
tasks.add(item);
Scheduler(() => {
tasks.delete(item);
runUpdates(() => {
Transition.running = true;
runTop(item);
}, false);
Transition && (Transition.running = false);
});
}
}
}
function runUserEffects(queue) {
let i, userLength = 0;
for (i = 0; i < queue.length; i++) {
const e = queue[i];
if (!e.user) runTop(e);
else queue[userLength++] = e;
}
if (sharedConfig.context) {
if (sharedConfig.count) {
sharedConfig.effects || (sharedConfig.effects = []);
sharedConfig.effects.push(...queue.slice(0, userLength));
return;
}
setHydrateContext();
}
if (sharedConfig.effects && (sharedConfig.done || !sharedConfig.count)) {
queue = [...sharedConfig.effects, ...queue];
userLength += sharedConfig.effects.length;
delete sharedConfig.effects;
}
for (i = 0; i < userLength; i++) runTop(queue[i]);
}
function lookUpstream(node, ignore) {
const runningTransition = Transition && Transition.running;
if (runningTransition) node.tState = 0;
else node.state = 0;
for (let i = 0; i < node.sources.length; i += 1) {
const source = node.sources[i];
if (source.sources) {
const state = runningTransition ? source.tState : source.state;
if (state === STALE) {
if (source !== ignore && (!source.updatedAt || source.updatedAt < ExecCount)) runTop(source);
} else if (state === PENDING) lookUpstream(source, ignore);
}
}
}
function markDownstream(node) {
const runningTransition = Transition && Transition.running;
for (let i = 0; i < node.observers.length; i += 1) {
const o = node.observers[i];
if (runningTransition ? !o.tState : !o.state) {
if (runningTransition) o.tState = PENDING;
else o.state = PENDING;
if (o.pure) Updates.push(o);
else Effects.push(o);
o.observers && markDownstream(o);
}
}
}
function cleanNode(node) {
let i;
if (node.sources) while (node.sources.length) {
const source = node.sources.pop(), index = node.sourceSlots.pop(), obs = source.observers;
if (obs && obs.length) {
const n = obs.pop(), s = source.observerSlots.pop();
if (index < obs.length) {
n.sourceSlots[s] = index;
obs[index] = n;
source.observerSlots[index] = s;
}
}
}
if (node.tOwned) {
for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]);
delete node.tOwned;
}
if (Transition && Transition.running && node.pure) reset(node, true);
else if (node.owned) {
for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]);
node.owned = null;
}
if (node.cleanups) {
for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i]();
node.cleanups = null;
}
if (Transition && Transition.running) node.tState = 0;
else node.state = 0;
}
function reset(node, top) {
if (!top) {
node.tState = 0;
Transition.disposed.add(node);
}
if (node.owned) for (let i = 0; i < node.owned.length; i++) reset(node.owned[i]);
}
function castError(err) {
if (err instanceof Error) return err;
return new Error(typeof err === "string" ? err : "Unknown error", { cause: err });
}
function runErrors(err, fns, owner) {
try {
for (const f of fns) f(err);
} catch (e) {
handleError(e, owner && owner.owner || null);
}
}
function handleError(err, owner = Owner) {
const fns = ERROR && owner && owner.context && owner.context[ERROR];
const error = castError(err);
if (!fns) throw error;
if (Effects) Effects.push({
fn() {
runErrors(error, fns, owner);
},
state: STALE
});
else runErrors(error, fns, owner);
}
var FALLBACK = Symbol("fallback");
function dispose(d) {
for (let i = 0; i < d.length; i++) d[i]();
}
function mapArray(list, mapFn, options = {}) {
let items = [], mapped = [], disposers = [], len = 0, indexes = mapFn.length > 1 ? [] : null;
onCleanup(() => dispose(disposers));
return () => {
let newItems = list() || [], newLen = newItems.length, i, j;
return untrack(() => {
let newIndices, newIndicesNext, temp, tempdisposers, tempIndexes, start, end, newEnd, item;
if (newLen === 0) {
if (len !== 0) {
dispose(disposers);
disposers = [];
items = [];
mapped = [];
len = 0;
indexes && (indexes = []);
}
if (options.fallback) {
items = [FALLBACK];
mapped[0] = createRoot((disposer) => {
disposers[0] = disposer;
return options.fallback();
});
len = 1;
}
} else if (len === 0) {
mapped = new Array(newLen);
for (j = 0; j < newLen; j++) {
items[j] = newItems[j];
mapped[j] = createRoot(mapper);
}
len = newLen;
} else {
temp = new Array(newLen);
tempdisposers = new Array(newLen);
indexes && (tempIndexes = new Array(newLen));
for (start = 0, end = Math.min(len, newLen); start < end && items[start] === newItems[start]; start++);
for (end = len - 1, newEnd = newLen - 1; end >= start && newEnd >= start && items[end] === newItems[newEnd]; end--, newEnd--) {
temp[newEnd] = mapped[end];
tempdisposers[newEnd] = disposers[end];
indexes && (tempIndexes[newEnd] = indexes[end]);
}
newIndices =  new Map();
newIndicesNext = new Array(newEnd + 1);
for (j = newEnd; j >= start; j--) {
item = newItems[j];
i = newIndices.get(item);
newIndicesNext[j] = i === void 0 ? -1 : i;
newIndices.set(item, j);
}
for (i = start; i <= end; i++) {
item = items[i];
j = newIndices.get(item);
if (j !== void 0 && j !== -1) {
temp[j] = mapped[i];
tempdisposers[j] = disposers[i];
indexes && (tempIndexes[j] = indexes[i]);
j = newIndicesNext[j];
newIndices.set(item, j);
} else disposers[i]();
}
for (j = start; j < newLen; j++) if (j in temp) {
mapped[j] = temp[j];
disposers[j] = tempdisposers[j];
if (indexes) {
indexes[j] = tempIndexes[j];
indexes[j](j);
}
} else mapped[j] = createRoot(mapper);
mapped = mapped.slice(0, len = newLen);
items = newItems.slice(0);
}
return mapped;
});
function mapper(disposer) {
disposers[j] = disposer;
if (indexes) {
const [s, set] = createSignal(j);
indexes[j] = set;
return mapFn(newItems[j], s);
}
return mapFn(newItems[j]);
}
};
}
var hydrationEnabled = false;
function createComponent(Comp, props) {
if (hydrationEnabled) {
if (sharedConfig.context) {
const c = sharedConfig.context;
setHydrateContext(nextHydrateContext());
const r = untrack(() => Comp(props || {}));
setHydrateContext(c);
return r;
}
}
return untrack(() => Comp(props || {}));
}
function trueFn() {
return true;
}
var propTraps = {
get(_, property, receiver) {
if (property === $PROXY) return receiver;
return _.get(property);
},
has(_, property) {
if (property === $PROXY) return true;
return _.has(property);
},
set: trueFn,
deleteProperty: trueFn,
getOwnPropertyDescriptor(_, property) {
return {
configurable: true,
enumerable: true,
get() {
return _.get(property);
},
set: trueFn,
deleteProperty: trueFn
};
},
ownKeys(_) {
return _.keys();
}
};
function resolveSource(s) {
return !(s = typeof s === "function" ? s() : s) ? {} : s;
}
function resolveSources() {
for (let i = 0, length = this.length; i < length; ++i) {
const v = this[i]();
if (v !== void 0) return v;
}
}
function mergeProps(...sources) {
let proxy = false;
for (let i = 0; i < sources.length; i++) {
const s = sources[i];
proxy = proxy || !!s && $PROXY in s;
sources[i] = typeof s === "function" ? (proxy = true, createMemo(s)) : s;
}
if (SUPPORTS_PROXY && proxy) return new Proxy({
get(property) {
for (let i = sources.length - 1; i >= 0; i--) {
const v = resolveSource(sources[i])[property];
if (v !== void 0) return v;
}
},
has(property) {
for (let i = sources.length - 1; i >= 0; i--) if (property in resolveSource(sources[i])) return true;
return false;
},
keys() {
const keys = [];
for (let i = 0; i < sources.length; i++) keys.push(...Object.keys(resolveSource(sources[i])));
return [...new Set(keys)];
}
}, propTraps);
const sourcesMap = {};
const defined = Object.create(null);
for (let i = sources.length - 1; i >= 0; i--) {
const source = sources[i];
if (!source) continue;
const sourceKeys = Object.getOwnPropertyNames(source);
for (let i = sourceKeys.length - 1; i >= 0; i--) {
const key = sourceKeys[i];
if (key === "__proto__" || key === "constructor") continue;
const desc = Object.getOwnPropertyDescriptor(source, key);
if (!defined[key]) defined[key] = desc.get ? {
enumerable: true,
configurable: true,
get: resolveSources.bind(sourcesMap[key] = [desc.get.bind(source)])
} : desc.value !== void 0 ? desc : void 0;
else {
const sources = sourcesMap[key];
if (sources) {
if (desc.get) sources.push(desc.get.bind(source));
else if (desc.value !== void 0) sources.push(() => desc.value);
}
}
}
}
const target = {};
const definedKeys = Object.keys(defined);
for (let i = definedKeys.length - 1; i >= 0; i--) {
const key = definedKeys[i], desc = defined[key];
if (desc && desc.get) Object.defineProperty(target, key, desc);
else target[key] = desc ? desc.value : void 0;
}
return target;
}
function splitProps(props, ...keys) {
const len = keys.length;
if (SUPPORTS_PROXY && $PROXY in props) {
const blocked = len > 1 ? keys.flat() : keys[0];
const res = keys.map((k) => {
return new Proxy({
get(property) {
return k.includes(property) ? props[property] : void 0;
},
has(property) {
return k.includes(property) && property in props;
},
keys() {
return k.filter((property) => property in props);
}
}, propTraps);
});
res.push(new Proxy({
get(property) {
return blocked.includes(property) ? void 0 : props[property];
},
has(property) {
return blocked.includes(property) ? false : property in props;
},
keys() {
return Object.keys(props).filter((k) => !blocked.includes(k));
}
}, propTraps));
return res;
}
const objects = [];
for (let i = 0; i <= len; i++) objects[i] = {};
for (const propName of Object.getOwnPropertyNames(props)) {
let keyIndex = len;
for (let i = 0; i < keys.length; i++) if (keys[i].includes(propName)) {
keyIndex = i;
break;
}
const desc = Object.getOwnPropertyDescriptor(props, propName);
!desc.get && !desc.set && desc.enumerable && desc.writable && desc.configurable ? objects[keyIndex][propName] = desc.value : Object.defineProperty(objects[keyIndex], propName, desc);
}
return objects;
}
var narrowedError = (name) => `Stale read from <${name}>.`;
function For(props) {
const fallback = "fallback" in props && { fallback: () => props.fallback };
return createMemo(mapArray(() => props.each, props.children, fallback || void 0));
}
function Show(props) {
const keyed = props.keyed;
const conditionValue = createMemo(() => props.when, void 0, void 0);
const condition = keyed ? conditionValue : createMemo(conditionValue, void 0, { equals: (a, b) => !a === !b });
return createMemo(() => {
const c = condition();
if (c) {
const child = props.children;
return typeof child === "function" && child.length > 0 ? untrack(() => child(keyed ? c : () => {
if (!untrack(condition)) throw narrowedError("Show");
return conditionValue();
})) : child;
}
return props.fallback;
}, void 0, void 0);
}
var Errors;
function ErrorBoundary$1(props) {
let err;
if (sharedConfig.context && sharedConfig.load) err = sharedConfig.load(sharedConfig.getContextId());
const [errored, setErrored] = createSignal(err, void 0);
Errors || (Errors =  new Set());
Errors.add(setErrored);
onCleanup(() => Errors.delete(setErrored));
return createMemo(() => {
let e;
if (e = errored()) {
const f = props.fallback;
return typeof f === "function" && f.length ? untrack(() => f(e, () => setErrored())) : f;
}
return catchError(() => props.children, setErrored);
}, void 0, void 0);
}
var INITIAL_NAVIGATION_STATE = {
lastSource: "auto-focus",
lastTimestamp: 0,
lastNavigatedIndex: null
};
var VALID_NAVIGATION_SOURCES = [
"button",
"click",
"keyboard",
"programmatic",
"scroll",
"auto-focus"
];
var [_lastSource, setLastSource] = createSignal(INITIAL_NAVIGATION_STATE.lastSource);
var [_lastTimestamp, setLastTimestamp] = createSignal(INITIAL_NAVIGATION_STATE.lastTimestamp);
var [_lastNavigatedIndex, setLastNavigatedIndex] = createSignal(INITIAL_NAVIGATION_STATE.lastNavigatedIndex);
var navigationSignals = {
get lastSource() {
return _lastSource();
},
set lastSource(v) {
setLastSource(v);
},
get lastTimestamp() {
return _lastTimestamp();
},
set lastTimestamp(v) {
setLastTimestamp(v);
},
get lastNavigatedIndex() {
return _lastNavigatedIndex();
},
set lastNavigatedIndex(v) {
setLastNavigatedIndex(v);
}
};
var resolveNowMs = (nowMs) => nowMs ?? Date.now();
var isValidNavigationSource = (value) => typeof value === "string" && VALID_NAVIGATION_SOURCES.includes(value);
var isManualSource = (source) => source === "button" || source === "keyboard";
var createNavigationActionError = (context, reason) =>  new Error(`[Gallery] Invalid navigation action (${context}): ${reason}`);
function validateNavigationParams(targetIndex, source, trigger, context) {
if (typeof targetIndex !== "number" || Number.isNaN(targetIndex)) throw createNavigationActionError(context, "Navigate payload targetIndex invalid");
if (!isValidNavigationSource(source)) throw createNavigationActionError(context, `Navigate payload source invalid: ${String(source)}`);
if (!isValidNavigationSource(trigger)) throw createNavigationActionError(context, `Navigate payload trigger invalid: ${String(trigger)}`);
}
function recordNavigation(targetIndex, source, nowMs) {
const timestamp = resolveNowMs(nowMs);
const currentIndex = navigationSignals.lastNavigatedIndex;
const currentSource = navigationSignals.lastSource;
if (targetIndex === currentIndex && isManualSource(source) && isManualSource(currentSource)) {
navigationSignals.lastTimestamp = timestamp;
return { isDuplicate: true };
}
navigationSignals.lastSource = source;
navigationSignals.lastTimestamp = timestamp;
navigationSignals.lastNavigatedIndex = targetIndex;
return { isDuplicate: false };
}
function resetNavigation(nowMs) {
navigationSignals.lastSource = INITIAL_NAVIGATION_STATE.lastSource;
navigationSignals.lastTimestamp = resolveNowMs(nowMs);
navigationSignals.lastNavigatedIndex = INITIAL_NAVIGATION_STATE.lastNavigatedIndex;
}
function resolveNavigationSource(trigger) {
if (trigger === "scroll") return "scroll";
if (trigger === "keyboard") return "keyboard";
return "button";
}
function effectSafe(fn) {
return createRoot((dispose) => {
createComputed(fn);
return dispose;
});
}
function createEventEmitter() {
const listeners =  new Map();
return {
on(event, callback) {
const eventListeners = listeners.get(event);
if (eventListeners) eventListeners.add(callback);
else listeners.set(event, new Set([callback]));
return () => {
const currentListeners = listeners.get(event);
if (currentListeners) currentListeners.delete(callback);
};
},
emit(event, data) {
const eventListeners = listeners.get(event);
if (!eventListeners) return;
for (const callback of eventListeners) try {
callback(data);
} catch {}
},
dispose() {
listeners.clear();
}
};
}
var batch = (fn) => batch$1(fn);
var INITIAL_STATE$1 = {
isOpen: false,
mediaItems: [],
currentIndex: 0,
isLoading: false,
error: null,
viewMode: "vertical"
};
var galleryIndexEvents = createEventEmitter();
var [isOpenSig, _setIsOpenInternal] = createSignal(INITIAL_STATE$1.isOpen);
function setIsOpen(value) {
_setIsOpenInternal(value);
}
var [mediaItemsSig, setMediaItems] = createSignal(INITIAL_STATE$1.mediaItems);
var [currentIndexSig, setCurrentIndex] = createSignal(INITIAL_STATE$1.currentIndex);
var [focusedIndexSig, setFocusedIndex] = createSignal(null);
var [currentVideoElementSig, setCurrentVideoElement] = createSignal(null);
var [_viewModeSig, _setViewMode] = createSignal(INITIAL_STATE$1.viewMode);
var [_isLoadingSig, _setIsLoading] = createSignal(INITIAL_STATE$1.isLoading);
var [_errorSig, _setErrorSig] = createSignal(INITIAL_STATE$1.error);
var gallerySignals = {
get isOpen() {
return isOpenSig();
},
get mediaItems() {
return mediaItemsSig();
},
get currentIndex() {
return currentIndexSig();
},
get isLoading() {
return _isLoadingSig();
},
get error() {
return _errorSig();
},
get viewMode() {
return _viewModeSig();
},
get focusedIndex() {
return focusedIndexSig();
},
get currentVideoElement() {
return currentVideoElementSig();
}
};
function setError(error) {
_setErrorSig(error);
if (error) _setIsLoading(false);
}
function applyGallerySessionUpdate(state) {
batch(() => {
setMediaItems(state.mediaItems);
setCurrentIndex(state.currentIndex);
setFocusedIndex(state.focusedIndex);
setCurrentVideoElement(state.currentVideoElement);
_setErrorSig(state.error);
setIsOpen(state.isOpen);
});
}
function applyGalleryStateUpdate(state) {
batch(() => {
setMediaItems(state.mediaItems);
setCurrentIndex(state.currentIndex);
_setIsLoading(state.isLoading);
_setErrorSig(state.error);
_setViewMode(state.viewMode);
setIsOpen(state.isOpen);
});
}
var galleryState = {
get value() {
return {
isOpen: isOpenSig(),
mediaItems: mediaItemsSig(),
currentIndex: currentIndexSig(),
isLoading: gallerySignals.isLoading,
error: gallerySignals.error,
viewMode: gallerySignals.viewMode
};
},
set value(state) {
applyGalleryStateUpdate(state);
},
subscribe(callback) {
return effectSafe(() => {
callback(galleryState.value);
});
}
};
function openGallery(items, startIndex = 0) {
const validIndex = clampIndex(startIndex, items.length);
applyGallerySessionUpdate({
isOpen: true,
mediaItems: items,
currentIndex: validIndex,
focusedIndex: validIndex,
currentVideoElement: null,
error: null
});
resetNavigation();
}
function closeGallery() {
applyGallerySessionUpdate({
isOpen: false,
currentIndex: 0,
mediaItems: [],
focusedIndex: null,
currentVideoElement: null,
error: null
});
resetNavigation();
}
function navigateNext(trigger = "click") {
const items = mediaItemsSig();
const current = currentIndexSig();
if (items.length <= 1) return;
const next = current + 1;
if (next >= items.length) return;
batch(() => {
setCurrentIndex(next);
setFocusedIndex(next);
});
recordNavigation(next, resolveNavigationSource(trigger));
galleryIndexEvents.emit("navigate:complete", {
index: next,
trigger
});
}
function navigatePrevious(trigger = "click") {
const items = mediaItemsSig();
const current = currentIndexSig();
if (items.length <= 1) return;
const prev = current - 1;
if (prev < 0) return;
batch(() => {
setCurrentIndex(prev);
setFocusedIndex(prev);
});
recordNavigation(prev, resolveNavigationSource(trigger));
galleryIndexEvents.emit("navigate:complete", {
index: prev,
trigger
});
}
function navigateToItem(targetIndex, trigger, source) {
validateNavigationParams(targetIndex, source, trigger, "navigateToItem");
const clampedIndex = clampIndex(targetIndex, mediaItemsSig().length);
if (clampedIndex === currentIndexSig()) return;
batch(() => {
setCurrentIndex(clampedIndex);
setFocusedIndex(clampedIndex);
});
recordNavigation(clampedIndex, source);
galleryIndexEvents.emit("navigate:complete", {
index: clampedIndex,
trigger
});
}
var playbackStateMap =  new WeakMap();
function executeVideoControl(action, options = {}) {
try {
const video = getGalleryVideo(options.video);
if (!video) return;
switch (action) {
case "play":
playVideo(video, options.context);
break;
case "pause":
pauseVideo(video);
break;
case "togglePlayPause":
togglePlayPause(video, options.context);
break;
case "volumeUp":
adjustVolume(video, .1);
break;
case "volumeDown":
adjustVolume(video, -.1);
break;
case "toggleMute":
video.muted = !video.muted;
break;
}
} catch (error) {
logger.error("Video control error", {
error,
action,
context: options.context
});
}
}
function getGalleryVideo(video) {
if (video instanceof HTMLVideoElement) return video;
const signaled = gallerySignals.currentVideoElement;
return signaled instanceof HTMLVideoElement ? signaled : null;
}
function playVideo(video, context) {
const promise = video.play?.();
if (promise && typeof promise.then === "function") promise.then(() => playbackStateMap.set(video, { playing: true })).catch(() => {
playbackStateMap.set(video, { playing: false });
});
else playbackStateMap.set(video, { playing: true });
}
function pauseVideo(video) {
video.pause?.();
playbackStateMap.set(video, { playing: false });
}
function togglePlayPause(video, context) {
if (playbackStateMap.get(video)?.playing ?? !video.paused) pauseVideo(video);
else playVideo(video, context);
}
function adjustVolume(video, delta) {
const newVolume = Math.max(0, Math.min(1, Math.round((video.volume + delta) * 100) / 100));
video.volume = newVolume;
if (newVolume > 0 && video.muted) video.muted = false;
if (newVolume === 0 && !video.muted) video.muted = true;
}
var keyboardDebounceState = {
lastExecutionTime: 0,
lastKey: ""
};
function shouldExecuteKeyboardAction(key, minIntervalMs) {
const now = Date.now();
const timeSinceLastExecution = now - keyboardDebounceState.lastExecutionTime;
if (key === keyboardDebounceState.lastKey && timeSinceLastExecution < minIntervalMs) return false;
keyboardDebounceState = {
lastExecutionTime: now,
lastKey: key
};
return true;
}
function resetKeyboardDebounceState() {
keyboardDebounceState = {
lastExecutionTime: 0,
lastKey: ""
};
}
var NAVIGATION_KEYS = new Set([
"Home",
"End",
"PageDown",
"PageUp",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"?"
]);
var VIDEO_CONTROL_KEYS = new Set([
"ArrowUp",
"ArrowDown",
"m",
"M"
]);
function handleKeyboardEvent(event, handlers, options) {
if (!options.enableKeyboard) return;
try {
const key = event.key;
const isGalleryOpen = gallerySignals.isOpen;
if (key === "Escape" && isGalleryOpen) {
handlers.onGalleryClose();
event.preventDefault();
event.stopPropagation();
return;
}
if (!isGalleryOpen) {
handlers.onKeyboardEvent?.(event);
return;
}
const isNavKey = NAVIGATION_KEYS.has(key) || key === "Space";
const isVideoKey = VIDEO_CONTROL_KEYS.has(key) || key === "Space";
if (!isNavKey && !isVideoKey) {
handlers.onKeyboardEvent?.(event);
return;
}
event.preventDefault();
event.stopPropagation();
if (key === "?") showKeyboardHelp();
else if (key === "Space") handleVideoControl(key);
else if ((key === "ArrowUp" || key === "ArrowDown") && gallerySignals.currentVideoElement) handleVideoControl(key);
else if (NAVIGATION_KEYS.has(key)) handleNavigation(key);
else handleVideoControl(key);
handlers.onKeyboardEvent?.(event);
} catch (error) {}
}
function handleNavigation(key) {
const current = gallerySignals.currentIndex;
const total = gallerySignals.mediaItems.length;
switch (key) {
case "ArrowLeft":
navigatePrevious("keyboard");
break;
case "ArrowRight":
navigateNext("keyboard");
break;
case "Home":
navigateToItem(0, "keyboard", "keyboard");
break;
case "End":
navigateToItem(Math.max(0, total - 1), "keyboard", "keyboard");
break;
case "PageUp":
navigateToItem(Math.max(0, current - 5), "keyboard", "keyboard");
break;
case "PageDown":
navigateToItem(Math.min(total - 1, current + 5), "keyboard", "keyboard");
break;
}
}
function handleVideoControl(key) {
switch (key) {
case "Space":
if (shouldExecuteKeyboardAction("Space", 150)) executeVideoControl("togglePlayPause");
break;
case "ArrowUp":
if (shouldExecuteKeyboardAction("ArrowUp", 100)) executeVideoControl("volumeUp");
break;
case "ArrowDown":
if (shouldExecuteKeyboardAction("ArrowDown", 100)) executeVideoControl("volumeDown");
break;
case "m":
case "M":
if (shouldExecuteKeyboardAction("M", 100)) executeVideoControl("toggleMute");
break;
}
}
function showKeyboardHelp() {
if (!shouldExecuteKeyboardAction("?", 500)) return;
try {
const lang = getLanguageService();
getUserscript().notification({
title: lang.translate("msg.kb.t"),
text: [
lang.translate("msg.kb.prev"),
lang.translate("msg.kb.next"),
lang.translate("msg.kb.cls"),
lang.translate("msg.kb.toggle")
].join("\n"),
timeout: 6e3
});
} catch {}
}
var VIDEO_CONTROL_DATASET_PREFIXES = [
"play",
"pause",
"mute",
"unmute",
"volume",
"slider",
"seek",
"scrub",
"progress"
];
var VIDEO_CONTROL_ROLES = ["slider", "progressbar"];
var VIDEO_CONTROL_ARIA_TOKENS = [
"volume",
"mute",
"unmute",
"seek",
"scrub",
"timeline",
"progress"
];
function createEventListener(handler) {
return (event) => {
handler(event);
};
}
function isHTMLElement(element) {
return element instanceof HTMLElement;
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
var GALLERY_SELECTORS = CSS.INTERNAL_SELECTORS;
var VIDEO_CONTROL_SELECTORS = [".video-controls", ".video-progress button"];
function safeClosest(element, selector) {
try {
return element.closest(selector);
} catch (error) {
return null;
}
}
function safeMatches(element, selector) {
try {
return element.matches(selector);
} catch (error) {
return false;
}
}
function containsControlToken(value, tokens) {
if (!value) return false;
const normalized = value.toLowerCase();
return tokens.some((token) => normalized.includes(token.toLowerCase()));
}
function getNearestAttributeValue(element, attribute) {
return safeClosest(element, `[${attribute}]`)?.getAttribute(attribute) ?? null;
}
function isWithinVideoPlayer(element) {
return safeClosest(element, VIDEO_PLAYER_CONTEXT_SELECTOR) !== null;
}
function matchesVideoControlSelectors(element) {
return VIDEO_CONTROL_SELECTORS.some((selector) => safeMatches(element, selector) || safeClosest(element, selector) !== null);
}
function isVideoControlElement(element) {
if (!isHTMLElement(element)) return false;
if (element.tagName.toLowerCase() === "video") return true;
if (typeof element.matches !== "function") return false;
if (matchesVideoControlSelectors(element)) return true;
if (containsControlToken(getNearestAttributeValue(element, "data-testid"), VIDEO_CONTROL_DATASET_PREFIXES)) return true;
if (containsControlToken(getNearestAttributeValue(element, "aria-label"), VIDEO_CONTROL_ARIA_TOKENS)) return true;
if (!isWithinVideoPlayer(element)) return false;
const role = element.getAttribute("role");
if (role && VIDEO_CONTROL_ROLES.includes(role.toLowerCase())) return true;
return safeMatches(element, "input[type=\"range\"]");
}
function isGalleryInternalElement(element) {
if (!isHTMLElement(element)) return false;
if (typeof element.matches !== "function") return false;
return GALLERY_SELECTORS.some((selector) => {
try {
return element.matches(selector) || element.closest(selector) !== null;
} catch (error) {
return false;
}
});
}
function isGalleryInternalEvent(event) {
const target = event.target;
if (!isHTMLElement(target)) return false;
return isGalleryInternalElement(target);
}
var MEDIA_LINK_SELECTOR = [
STATUS_LINK_SELECTOR,
"a[href*=\"/photo/\"]",
"a[href*=\"/video/\"]"
].join(", ");
var MEDIA_CONTAINER_SELECTOR = STABLE_MEDIA_CONTAINERS_SELECTORS.join(", ");
var INTERACTIVE_SELECTOR = [
"button",
"a",
"[role=\"button\"]",
"[data-testid=\"like\"]",
"[data-testid=\"retweet\"]",
"[data-testid=\"reply\"]",
"[data-testid=\"share\"]",
"[data-testid=\"bookmark\"]"
].join(", ");
function isValidMediaSource(url) {
if (!url) return false;
if (url.startsWith("blob:")) return true;
return isValidMediaUrl(url);
}
var TWITTER_HOSTNAME_PATTERN = /(^|\.)(?:x|twitter)\.com$/iu;
function isSupportedStatusMediaPath(pathname) {
return /\/status\/\d+/iu.test(pathname) || /\/photo\/\d+/iu.test(pathname) || /\/video\/\d+/iu.test(pathname);
}
function isNativeStatusMediaLink(href) {
if (!href) return false;
const parsed = tryParseUrl(href);
if (!parsed) return false;
if (!TWITTER_HOSTNAME_PATTERN.test(parsed.hostname)) return false;
return isSupportedStatusMediaPath(parsed.pathname);
}
function isMediaCard(cardWrapper) {
const cardLinks = cardWrapper.querySelectorAll("a[href]");
for (const link of cardLinks) if (!isNativeStatusMediaLink(link.getAttribute("href"))) return false;
if (cardWrapper.querySelectorAll("img[src*=\"pbs.twimg.com/card_img\"]").length > 0) return true;
return cardWrapper.querySelector("img, video") !== null;
}
function shouldBlockMediaTrigger(target) {
if (!target) return false;
if (isVideoControlElement(target)) return true;
if (target.closest(CSS.SELECTORS.ROOT) || target.closest(CSS.SELECTORS.OVERLAY)) return true;
const cardWrapper = target.closest("[data-testid=\"card.wrapper\"]");
if (cardWrapper instanceof HTMLElement) {
if (isMediaCard(cardWrapper)) return false;
return true;
}
const blockedContextSelector = [
"[data-testid=\"twitterArticleReadView\"]",
"[data-testid=\"longformRichTextComponent\"]",
"[data-testid=\"twitterArticleRichTextView\"]",
"[data-testid=\"article-cover-image\"]",
...STABLE_MEDIA_VIEWERS_SELECTORS,
"[data-testid=\"swipe-to-dismiss\"]",
"[data-testid=\"mask\"]"
].join(", ");
if (target.closest(blockedContextSelector)) return true;
const interactive = target.closest(INTERACTIVE_SELECTOR);
if (interactive) {
const matchesMediaLinkSelector = interactive instanceof HTMLAnchorElement ? isNativeStatusMediaLink(interactive.getAttribute("href")) : interactive.matches(MEDIA_LINK_SELECTOR);
if (interactive.tagName === "A" && !matchesMediaLinkSelector) return true;
const matchesMediaContainerSelector = interactive.matches(MEDIA_CONTAINER_SELECTOR);
const hasMediaContainerDescendant = interactive.querySelector(MEDIA_CONTAINER_SELECTOR) !== null;
return !(matchesMediaLinkSelector || matchesMediaContainerSelector || hasMediaContainerDescendant);
}
return false;
}
function isProcessableMedia(target) {
if (!target) return false;
if (gallerySignals.isOpen) return false;
if (shouldBlockMediaTrigger(target)) return false;
const mediaElement = findMediaElementInDOM(target);
if (mediaElement) {
if (isValidMediaSource(extractMediaUrlFromElement(mediaElement))) return true;
}
return !!target.closest(MEDIA_CONTAINER_SELECTOR);
}
async function handleMediaClick(event, handlers, options) {
if (!options.enableMediaDetection) return {
handled: false,
reason: "Media detection disabled"
};
const target = event.target;
if (!isHTMLElement(target)) return {
handled: false,
reason: "Invalid target (not HTMLElement)"
};
if (gallerySignals.isOpen && isGalleryInternalElement(target)) return {
handled: false,
reason: "Gallery internal event"
};
if (isVideoControlElement(target)) return {
handled: false,
reason: "Video control element"
};
if (!isProcessableMedia(target)) return {
handled: false,
reason: "Non-processable media"
};
event.stopImmediatePropagation();
event.preventDefault();
await handlers.onMediaClick(target, event);
return {
handled: true,
reason: "Media click processed"
};
}
var DEFAULT_GALLERY_EVENT_OPTIONS = {
enableKeyboard: true,
enableMediaDetection: true,
debugMode: false,
preventBubbling: true,
context: "gallery"
};
var lifecycleState$1 = {
initialized: false,
context: null
};
function sanitizeContext(context) {
const trimmed = context?.trim();
return trimmed && trimmed.length > 0 ? trimmed : DEFAULT_GALLERY_EVENT_OPTIONS.context;
}
function resolveInitializationInput(optionsOrRoot) {
if (optionsOrRoot instanceof HTMLElement) return {
options: { ...DEFAULT_GALLERY_EVENT_OPTIONS },
root: optionsOrRoot
};
const partial = optionsOrRoot ?? {};
return {
options: {
...DEFAULT_GALLERY_EVENT_OPTIONS,
...partial,
context: sanitizeContext(partial.context)
},
root: null
};
}
function resolveEventTarget(explicitRoot) {
return explicitRoot || document.body || document.documentElement || document;
}
function registerListeners(eventManager, target, handlers, options, context) {
const listenerOptions = {
capture: true,
passive: false
};
if (options.enableKeyboard) {
const keyHandler = (evt) => {
handleKeyboardEvent(evt, handlers, options);
};
eventManager.addEventListener(target, "keydown", keyHandler, {
...listenerOptions,
context
});
}
if (options.enableMediaDetection) {
const clickHandler = async (evt) => {
await handleMediaClick(evt, handlers, options);
};
eventManager.addEventListener(target, "click", clickHandler, {
...listenerOptions,
context
});
}
}
async function initializeGalleryEvents(handlers, optionsOrRoot) {
if (lifecycleState$1.initialized) cleanupGalleryEvents();
if (!handlers) return cleanupGalleryEvents;
const { options: finalOptions, root: explicitGalleryRoot } = resolveInitializationInput(optionsOrRoot);
const context = sanitizeContext(finalOptions.context);
const target = resolveEventTarget(explicitGalleryRoot);
registerListeners(EventManager.getInstance(), target, handlers, finalOptions, context);
resetKeyboardDebounceState();
lifecycleState$1 = {
initialized: true,
context
};
return cleanupGalleryEvents;
}
function cleanupGalleryEvents() {
if (!lifecycleState$1.initialized) return;
if (lifecycleState$1.context) EventManager.getInstance().removeByContext(lifecycleState$1.context);
resetKeyboardDebounceState();
lifecycleState$1 = {
initialized: false,
context: null
};
}
var ZERO_RESULT = {
pausedCount: 0,
totalCandidates: 0,
skippedCount: 0
};
function resolveRoot(root) {
if (root && typeof root.querySelectorAll === "function") return root;
return typeof document !== "undefined" && typeof document.querySelectorAll === "function" ? document : null;
}
function isVideoPlaying(video) {
try {
return !video.paused && !video.ended;
} catch {
return false;
}
}
function shouldPauseVideo(video, force = false) {
return video instanceof HTMLVideoElement && !isGalleryInternalElement(video) && video.isConnected && (force || isVideoPlaying(video));
}
function tryPauseVideo(video) {
try {
video.pause?.();
return true;
} catch (error) {
return false;
}
}
function pauseActiveTwitterVideos(options = {}) {
const root = resolveRoot(options.root ?? null);
if (!root) return ZERO_RESULT;
const videos = Array.from(root.querySelectorAll("video"));
const inspectedCount = videos.length;
if (inspectedCount === 0) return ZERO_RESULT;
let pausedCount = 0;
let totalCandidates = 0;
for (const video of videos) {
if (!shouldPauseVideo(video, options.force)) continue;
totalCandidates += 1;
if (tryPauseVideo(video)) pausedCount += 1;
}
const result = {
pausedCount,
totalCandidates,
skippedCount: inspectedCount - pausedCount
};
if (result.pausedCount > 0) {}
return result;
}
var VIDEO_TRIGGER_SELECTORS = VIDEO_CONTAINER_SELECTORS;
var IMAGE_TRIGGER_SELECTORS = IMAGE_CONTAINER_SELECTORS;
var PAUSE_RESULT_DEFAULT = {
pausedCount: 0,
totalCandidates: 0,
skippedCount: 0
};
function findTweetContainer(element) {
if (!element) return null;
return closestWithFallback(element, TWEET_CONTAINER_SELECTORS, { debugLabel: "tweet-container" });
}
function resolvePauseContext(request) {
if (request.root !== void 0) return {
root: request.root ?? null,
scope: "custom"
};
const tweetContainer = findTweetContainer(request.sourceElement);
if (tweetContainer) return {
root: tweetContainer,
scope: "tweet"
};
return {
root: null,
scope: "document"
};
}
function isVideoTriggerElement(element) {
if (!element) return false;
if (element.tagName === "VIDEO") return true;
return closestWithFallback(element, VIDEO_TRIGGER_SELECTORS, { debugLabel: "video-container" }) !== null;
}
function isImageTriggerElement(element) {
if (!element) return false;
if (element.tagName === "IMG") return true;
return closestWithFallback(element, IMAGE_TRIGGER_SELECTORS, { debugLabel: "image-container" }) !== null;
}
function inferAmbientVideoTrigger(element) {
if (isVideoTriggerElement(element)) return "video-click";
if (isImageTriggerElement(element)) return "image-click";
return "unknown";
}
function pauseAmbientVideosForGallery(request = {}) {
const trigger = request.trigger ?? inferAmbientVideoTrigger(request.sourceElement);
const force = request.force ?? true;
const reason = request.reason ?? trigger;
const { root, scope } = resolvePauseContext(request);
let result;
try {
result = pauseActiveTwitterVideos({
root,
force
});
} catch (error) {
return {
...PAUSE_RESULT_DEFAULT,
failed: true,
trigger,
forced: force,
reason,
scope
};
}
if (result.totalCandidates > 0 || result.pausedCount > 0) {}
return {
...result,
failed: false,
trigger,
forced: force,
reason,
scope
};
}
var guardDispose = null;
var guardSubscribers = 0;
var ensureGuardEffect = () => {
if (guardDispose) return;
guardDispose = effectSafe(() => {
if (!gallerySignals.isOpen) return;
if (pauseAmbientVideosForGallery({
trigger: "guard",
reason: "guard"
}).pausedCount <= 0) return;
});
};
var startAmbientVideoGuard = () => {
guardSubscribers += 1;
ensureGuardEffect();
return stopAmbientVideoGuard;
};
var stopAmbientVideoGuard = () => {
if (guardSubscribers === 0) return;
guardSubscribers -= 1;
if (guardSubscribers > 0) return;
guardDispose?.();
guardDispose = null;
};
var GalleryApp = class {
isInitialized = false;
get userscript() {
return getUserscript();
}
ambientVideoGuardDispose = null;
constructor() {}
async initialize() {
if (this.isInitialized) return;
try {
await this.setupEventHandlers();
this.ambientVideoGuardDispose = this.ambientVideoGuardDispose ?? startAmbientVideoGuard();
this.isInitialized = true;
} catch (error) {
galleryErrorReporter.critical(error, { code: "GALLERY_APP_INIT_FAILED" });
throw error;
}
}
async retryInitialize() {
if (this.isInitialized) return;
return this.initialize();
}
async setupEventHandlers() {
const enableKeyboardSetting = tryGetSettingsManager()?.get?.("gallery.enableKeyboardNav");
await initializeGalleryEvents({
onMediaClick: (element, event) => this.handleMediaClick(element, event),
onGalleryClose: () => this.closeGallery(),
onKeyboardEvent: (event) => {
if (event.key === "Escape" && gallerySignals.isOpen) this.closeGallery();
}
}, {
enableKeyboard: typeof enableKeyboardSetting === "boolean" ? enableKeyboardSetting : true,
enableMediaDetection: true,
debugMode: false,
preventBubbling: true,
context: "gallery"
});
}
async handleMediaClick(element, _event) {
try {
const result = await getMediaService().extractFromClickedElement(element);
if (result.success && result.mediaItems.length > 0) await this.openGallery(result.mediaItems, result.clickedIndex, { pauseContext: {
sourceElement: element,
reason: "media-click"
} });
else {
mediaErrorReporter.warn(  new Error("Media extraction returned no items"), {
code: "MEDIA_EXTRACTION_EMPTY",
metadata: { success: result.success }
});
this.userscript.notification({
title: "Failed to load media",
text: "Could not find images or videos."
});
}
} catch (error) {
mediaErrorReporter.error(error, { code: "MEDIA_EXTRACTION_ERROR" });
this.userscript.notification({
title: "Error occurred",
text: normalizeErrorMessage(error)
});
}
}
async openGallery(mediaItems, startIndex = 0, options = {}) {
if (!this.isInitialized) try {
await this.retryInitialize();
} catch {
this.userscript.notification({
title: "Gallery unavailable",
text: "Userscript manager required."
});
return;
}
if (mediaItems.length === 0) return;
try {
const validIndex = clampIndex(startIndex, mediaItems.length);
const providedContext = options.pauseContext ?? null;
pauseAmbientVideosForGallery({
...providedContext,
reason: providedContext?.reason ?? (providedContext ? "media-click" : "programmatic")
});
openGallery(mediaItems, validIndex);
} catch (error) {
galleryErrorReporter.error(error, {
code: "GALLERY_OPEN_FAILED",
metadata: {
itemCount: mediaItems.length,
startIndex
}
});
this.userscript.notification({
title: "Failed to load gallery",
text: normalizeErrorMessage(error)
});
throw error;
}
}
closeGallery() {
try {
if (gallerySignals.isOpen) closeGallery();
} catch (error) {
galleryErrorReporter.error(error, { code: "GALLERY_CLOSE_FAILED" });
}
}
async cleanup() {
try {
if (gallerySignals.isOpen) this.closeGallery();
this.ambientVideoGuardDispose?.();
this.ambientVideoGuardDispose = null;
try {
cleanupGalleryEvents();
} catch (error) {}
this.isInitialized = false;
delete globalThis.xegGalleryDebug;
} catch (error) {
galleryErrorReporter.error(error, { code: "GALLERY_CLEANUP_FAILED" });
}
}
};
var memo = (fn) => createMemo(() => fn());
function reconcileArrays(parentNode, a, b) {
let bLength = b.length, aEnd = a.length, bEnd = bLength, aStart = 0, bStart = 0, after = a[aEnd - 1].nextSibling, map = null;
while (aStart < aEnd || bStart < bEnd) {
if (a[aStart] === b[bStart]) {
aStart++;
bStart++;
continue;
}
while (a[aEnd - 1] === b[bEnd - 1]) {
aEnd--;
bEnd--;
}
if (aEnd === aStart) {
const node = bEnd < bLength ? bStart ? b[bStart - 1].nextSibling : b[bEnd - bStart] : after;
while (bStart < bEnd) parentNode.insertBefore(b[bStart++], node);
} else if (bEnd === bStart) while (aStart < aEnd) {
if (!map || !map.has(a[aStart])) a[aStart].remove();
aStart++;
}
else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
const node = a[--aEnd].nextSibling;
parentNode.insertBefore(b[bStart++], a[aStart++].nextSibling);
parentNode.insertBefore(b[--bEnd], node);
a[aEnd] = b[bEnd];
} else {
if (!map) {
map =  new Map();
let i = bStart;
while (i < bEnd) map.set(b[i], i++);
}
const index = map.get(a[aStart]);
if (index != null) if (bStart < index && index < bEnd) {
let i = aStart, sequence = 1, t;
while (++i < aEnd && i < bEnd) {
if ((t = map.get(a[i])) == null || t !== index + sequence) break;
sequence++;
}
if (sequence > index - bStart) {
const node = a[aStart];
while (bStart < index) parentNode.insertBefore(b[bStart++], node);
} else parentNode.replaceChild(b[bStart++], a[aStart++]);
} else aStart++;
else a[aStart++].remove();
}
}
}
var $$EVENTS = "_$DX_DELEGATE";
function render(code, element, init, options = {}) {
let disposer;
createRoot((dispose) => {
disposer = dispose;
element === document ? code() : insert(element, code(), element.firstChild ? null : void 0, init);
}, options.owner);
return () => {
disposer();
element.textContent = "";
};
}
function template(html, isImportNode, isSVG, isMathML) {
let node;
const create = () => {
const t = isMathML ? document.createElementNS("http://www.w3.org/1998/Math/MathML", "template") : document.createElement("template");
t.innerHTML = html;
return isSVG ? t.content.firstChild.firstChild : isMathML ? t.firstChild : t.content.firstChild;
};
const fn = isImportNode ? () => untrack(() => document.importNode(node || (node = create()), true)) : () => (node || (node = create())).cloneNode(true);
fn.cloneNode = fn;
return fn;
}
function delegateEvents(eventNames, document = window.document) {
const e = document[$$EVENTS] || (document[$$EVENTS] =  new Set());
for (let i = 0, l = eventNames.length; i < l; i++) {
const name = eventNames[i];
if (!e.has(name)) {
e.add(name);
document.addEventListener(name, eventHandler);
}
}
}
function setAttribute(node, name, value) {
if (isHydrating(node)) return;
if (value == null) node.removeAttribute(name);
else node.setAttribute(name, value);
}
function className(node, value) {
if (isHydrating(node)) return;
if (value == null) node.removeAttribute("class");
else node.className = value;
}
function addEventListener(node, name, handler, delegate) {
if (delegate) if (Array.isArray(handler)) {
node[`$$${name}`] = handler[0];
node[`$$${name}Data`] = handler[1];
} else node[`$$${name}`] = handler;
else if (Array.isArray(handler)) {
const handlerFn = handler[0];
node.addEventListener(name, handler[0] = (e) => handlerFn.call(node, handler[1], e));
} else node.addEventListener(name, handler, typeof handler !== "function" && handler);
}
function style(node, value, prev) {
if (!value) return prev ? setAttribute(node, "style") : value;
const nodeStyle = node.style;
if (typeof value === "string") return nodeStyle.cssText = value;
typeof prev === "string" && (nodeStyle.cssText = prev = void 0);
prev || (prev = {});
value || (value = {});
let v, s;
for (s in prev) {
value[s] ?? nodeStyle.removeProperty(s);
delete prev[s];
}
for (s in value) {
v = value[s];
if (v !== prev[s]) {
nodeStyle.setProperty(s, v);
prev[s] = v;
}
}
return prev;
}
function setStyleProperty(node, name, value) {
value != null ? node.style.setProperty(name, value) : node.style.removeProperty(name);
}
function use(fn, element, arg) {
return untrack(() => fn(element, arg));
}
function insert(parent, accessor, marker, initial) {
if (marker !== void 0 && !initial) initial = [];
if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);
createRenderEffect((current) => insertExpression(parent, accessor(), current, marker), initial);
}
function isHydrating(node) {
return !!sharedConfig.context && !sharedConfig.done && (!node || node.isConnected);
}
function eventHandler(e) {
if (sharedConfig.registry && sharedConfig.events) {
if (sharedConfig.events.find(([el, ev]) => ev === e)) return;
}
let node = e.target;
const key = `$$${e.type}`;
const oriTarget = e.target;
const oriCurrentTarget = e.currentTarget;
const retarget = (value) => Object.defineProperty(e, "target", {
configurable: true,
value
});
const handleNode = () => {
const handler = node[key];
if (handler && !node.disabled) {
const data = node[`${key}Data`];
data !== void 0 ? handler.call(node, data, e) : handler.call(node, e);
if (e.cancelBubble) return;
}
node.host && typeof node.host !== "string" && !node.host._$host && node.contains(e.target) && retarget(node.host);
return true;
};
const walkUpTree = () => {
while (handleNode() && (node = node._$host || node.parentNode || node.host));
};
Object.defineProperty(e, "currentTarget", {
configurable: true,
get() {
return node || document;
}
});
if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = _$HY.done = true;
if (e.composedPath) {
const path = e.composedPath();
retarget(path[0]);
for (let i = 0; i < path.length - 2; i++) {
node = path[i];
if (!handleNode()) break;
if (node._$host) {
node = node._$host;
walkUpTree();
break;
}
if (node.parentNode === oriCurrentTarget) break;
}
} else walkUpTree();
retarget(oriTarget);
}
function insertExpression(parent, value, current, marker, unwrapArray) {
const hydrating = isHydrating(parent);
if (hydrating) {
!current && (current = [...parent.childNodes]);
let cleaned = [];
for (let i = 0; i < current.length; i++) {
const node = current[i];
if (node.nodeType === 8 && node.data.slice(0, 2) === "!$") node.remove();
else cleaned.push(node);
}
current = cleaned;
}
while (typeof current === "function") current = current();
if (value === current) return current;
const t = typeof value, multi = marker !== void 0;
parent = multi && current[0] && current[0].parentNode || parent;
if (t === "string" || t === "number") {
if (hydrating) return current;
if (t === "number") {
value = value.toString();
if (value === current) return current;
}
if (multi) {
let node = current[0];
if (node && node.nodeType === 3) node.data !== value && (node.data = value);
else node = document.createTextNode(value);
current = cleanChildren(parent, current, marker, node);
} else if (current !== "" && typeof current === "string") current = parent.firstChild.data = value;
else current = parent.textContent = value;
} else if (value == null || t === "boolean") {
if (hydrating) return current;
current = cleanChildren(parent, current, marker);
} else if (t === "function") {
createRenderEffect(() => {
let v = value();
while (typeof v === "function") v = v();
current = insertExpression(parent, v, current, marker);
});
return () => current;
} else if (Array.isArray(value)) {
const array = [];
const currentArray = current && Array.isArray(current);
if (normalizeIncomingArray(array, value, current, unwrapArray)) {
createRenderEffect(() => current = insertExpression(parent, array, current, marker, true));
return () => current;
}
if (hydrating) {
if (!array.length) return current;
if (marker === void 0) return current = [...parent.childNodes];
let node = array[0];
if (node.parentNode !== parent) return current;
const nodes = [node];
while ((node = node.nextSibling) !== marker) nodes.push(node);
return current = nodes;
}
if (array.length === 0) {
current = cleanChildren(parent, current, marker);
if (multi) return current;
} else if (currentArray) if (current.length === 0) appendNodes(parent, array, marker);
else reconcileArrays(parent, current, array);
else {
current && cleanChildren(parent);
appendNodes(parent, array);
}
current = array;
} else if (value.nodeType) {
if (hydrating && value.parentNode) return current = multi ? [value] : value;
if (Array.isArray(current)) {
if (multi) return current = cleanChildren(parent, current, marker, value);
cleanChildren(parent, current, null, value);
} else if (current == null || current === "" || !parent.firstChild) parent.appendChild(value);
else parent.replaceChild(value, parent.firstChild);
current = value;
}
return current;
}
function normalizeIncomingArray(normalized, array, current, unwrap) {
let dynamic = false;
for (let i = 0, len = array.length; i < len; i++) {
let item = array[i], prev = current && current[normalized.length], t;
if (item == null || item === true || item === false);
else if ((t = typeof item) === "object" && item.nodeType) normalized.push(item);
else if (Array.isArray(item)) dynamic = normalizeIncomingArray(normalized, item, prev) || dynamic;
else if (t === "function") if (unwrap) {
while (typeof item === "function") item = item();
dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item], Array.isArray(prev) ? prev : [prev]) || dynamic;
} else {
normalized.push(item);
dynamic = true;
}
else {
const value = String(item);
if (prev && prev.nodeType === 3 && prev.data === value) normalized.push(prev);
else normalized.push(document.createTextNode(value));
}
}
return dynamic;
}
function appendNodes(parent, array, marker = null) {
for (let i = 0, len = array.length; i < len; i++) parent.insertBefore(array[i], marker);
}
function cleanChildren(parent, current, marker, replacement) {
if (marker === void 0) return parent.textContent = "";
const node = replacement || document.createTextNode("");
if (current.length) {
let inserted = false;
for (let i = current.length - 1; i >= 0; i--) {
const el = current[i];
if (node !== el) {
const isParent = el.parentNode === parent;
if (!inserted && !i) isParent ? parent.replaceChild(node, el) : parent.insertBefore(node, marker);
else isParent && el.remove();
} else inserted = true;
}
} else parent.insertBefore(node, marker);
return [node];
}
function useGalleryFitMode(options) {
const { scrollToCurrentItem, currentIndex } = options;
const getInitialFitMode = () => {
return getTypedSettingOr("gallery.imageFitMode", "fitWidth");
};
const [imageFitMode, setImageFitMode] = createSignal(getInitialFitMode());
const persistFitMode = (mode) => setTypedSetting("gallery.imageFitMode", mode).catch((error) => {});
const applyFitMode = (mode, event) => {
event?.preventDefault();
event?.stopPropagation();
setImageFitMode(mode);
persistFitMode(mode);
scrollToCurrentItem();
navigateToItem(currentIndex(), "programmatic", "auto-focus");
};
const handleFitOriginal = (event) => applyFitMode("original", event);
const handleFitWidth = (event) => applyFitMode("fitWidth", event);
const handleFitHeight = (event) => applyFitMode("fitHeight", event);
const handleFitContainer = (event) => applyFitMode("fitContainer", event);
return {
imageFitMode,
handleFitOriginal,
handleFitWidth,
handleFitHeight,
handleFitContainer
};
}
function useGalleryKeyboard({ onClose }) {
createEffect(() => {
if (typeof document === "undefined") return;
const isEditableTarget = (target) => {
const element = target;
if (!element) return false;
const tag = element.tagName?.toUpperCase();
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || !!element.isContentEditable;
};
const handleKeyDown = (event) => {
const keyboardEvent = event;
if (isEditableTarget(keyboardEvent.target)) return;
if (keyboardEvent.key === "Escape") {
keyboardEvent.preventDefault();
keyboardEvent.stopPropagation();
onClose();
}
};
const eventManager = EventManager.getInstance();
const listenerId = eventManager.addEventListener(document, "keydown", handleKeyDown, {
capture: true,
context: "gallery-keyboard-navigation"
});
onCleanup(() => {
if (listenerId) eventManager.removeListener(listenerId);
});
});
}
function useGalleryNavigationHandlers(options) {
const { currentIndex, mediaItems, onClose } = options;
const handlePrevious = () => {
const current = currentIndex();
if (current > 0) navigateToItem(current - 1, "click", "button");
};
const handleNext = () => {
const current = currentIndex();
if (current < mediaItems().length - 1) navigateToItem(current + 1, "click", "button");
};
const handleBackgroundClick = (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (target.closest("[data-role=\"toolbar\"], [data-role=\"toolbar-hover-zone\"], [data-gallery-element], [data-xeg-role=\"gallery-item\"], [data-xeg-role=\"scroll-spacer\"]")) return;
onClose();
};
const handleMediaItemClick = (index) => {
const items = mediaItems();
const current = currentIndex();
if (index >= 0 && index < items.length && index !== current) navigateToItem(index, "click", "scroll");
};
return {
handlePrevious,
handleNext,
handleBackgroundClick,
handleMediaItemClick
};
}
function createDebounced(fn, delayMs = 300) {
let timeoutId = null;
let pendingArgs = null;
const cancel = () => {
if (timeoutId !== null) {
globalTimerManager.clearTimeout(timeoutId);
timeoutId = null;
}
pendingArgs = null;
};
const flush = () => {
if (timeoutId !== null && pendingArgs !== null) {
globalTimerManager.clearTimeout(timeoutId);
const args = pendingArgs;
timeoutId = null;
pendingArgs = null;
fn(...args);
}
};
const debounced = ((...args) => {
cancel();
pendingArgs = args;
timeoutId = globalTimerManager.setTimeout(() => {
const savedArgs = pendingArgs;
timeoutId = null;
pendingArgs = null;
if (savedArgs !== null) fn(...savedArgs);
}, delayMs);
});
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
function useGalleryScrollCorrection(options) {
const { isVisible, currentIndex, activeMedia, scrollToItem } = options;
const debouncedScrollCorrection = createDebounced((index, mediaId) => {
if (!isVisible()) return;
if (index !== currentIndex() || activeMedia()?.id !== mediaId) return;
scrollToItem(index);
}, 120);
onCleanup(() => {
debouncedScrollCorrection.cancel();
});
return { debouncedScrollCorrection };
}
function useGalleryWheelRedirect(options) {
const { containerEl, itemsContainerEl } = options;
createEffect(() => {
const container = containerEl();
if (!container) return;
const controller = new AbortController();
const handleContainerWheel = (event) => {
const itemsContainer = itemsContainerEl();
if (!itemsContainer) return;
const target = event.target;
if (!(target instanceof Element)) return;
if (itemsContainer.contains(target)) return;
event.preventDefault();
event.stopPropagation();
itemsContainer.scrollTop += event.deltaY;
};
const eventManager = EventManager.getInstance();
const listener = (event) => {
handleContainerWheel(event);
};
eventManager.addEventListener(container, "wheel", listener, {
passive: false,
signal: controller.signal,
context: "gallery:wheel:container-redirect"
});
onCleanup(() => controller.abort());
});
}
var observerRegistry =  new WeakMap();
var SharedObserver = {
observe(element, callback, options = {}) {
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) try {
callback(entry);
} catch (error) {}
}, options);
observer.observe(element);
let set = observerRegistry.get(element);
if (!set) {
set =  new Set();
observerRegistry.set(element, set);
}
set.add(observer);
let isActive = true;
const unsubscribe = () => {
if (!isActive) return;
isActive = false;
observer.unobserve(element);
observer.disconnect();
const currentSet = observerRegistry.get(element);
if (currentSet) {
currentSet.delete(observer);
if (currentSet.size === 0) observerRegistry.delete(element);
}
};
return unsubscribe;
},
unobserve(element) {
const set = observerRegistry.get(element);
if (!set || set.size === 0) return;
for (const observer of set) observer.disconnect();
observerRegistry.delete(element);
}
};
var DEFAULTS$1 = {
THRESHOLD: [
0,
.5,
1
],
ROOT_MARGIN: "0px"
};
var FocusCoordinator = class {
items =  new Map();
observerOptions;
_rafId = null;
constructor(options) {
this.options = options;
const threshold = options.threshold;
let resolvedThreshold;
if (typeof threshold === "number") resolvedThreshold = threshold;
else if (Array.isArray(threshold)) resolvedThreshold = [...threshold];
else resolvedThreshold = [...DEFAULTS$1.THRESHOLD];
this.observerOptions = {
threshold: resolvedThreshold,
rootMargin: options.rootMargin ?? DEFAULTS$1.ROOT_MARGIN
};
}
registerItem(index, element) {
this.items.get(index)?.unsubscribe?.();
if (!element) {
this.items.delete(index);
return;
}
const trackedItem = {
element,
isVisible: false
};
trackedItem.unsubscribe = SharedObserver.observe(element, (entry) => {
const item = this.items.get(index);
if (item) {
item.entry = entry;
item.isVisible = entry.isIntersecting;
}
}, this.observerOptions);
this.items.set(index, trackedItem);
}
updateFocus(force = false) {
if (!force && !this.options.isEnabled()) return;
if (this._rafId !== null) return;
this._rafId = requestAnimationFrame(() => {
this._rafId = null;
const container = this.options.container();
if (!container) return;
const selection = this.selectBestCandidate(container.getBoundingClientRect());
if (!selection) return;
if (this.options.activeIndex() !== selection.index) this.options.onFocusChange(selection.index, "auto");
});
}
cleanup() {
if (this._rafId !== null) {
cancelAnimationFrame(this._rafId);
this._rafId = null;
}
for (const item of this.items.values()) item.unsubscribe?.();
this.items.clear();
}
selectBestCandidate(containerRect) {
const viewportHeight = Math.max(containerRect.height, 1);
const viewportTop = containerRect.top;
const viewportBottom = viewportTop + viewportHeight;
const viewportCenter = viewportTop + viewportHeight / 2;
const topProximityThreshold = 50;
const itemRects =  new Map();
for (const [index, item] of this.items) {
if (!item.isVisible || !item.element.isConnected) continue;
const rect = item.element.getBoundingClientRect();
const top = rect.top;
const height = rect.height;
itemRects.set(index, {
top,
height,
bottom: top + height,
center: top + height / 2
});
}
let bestCandidate = null;
let topAlignedCandidate = null;
let highestVisibilityCandidate = null;
for (const [index, itemRect] of itemRects) {
const itemTop = itemRect.top;
const itemHeight = itemRect.height;
const itemBottom = itemRect.bottom;
const itemCenter = itemRect.center;
const visibleHeight = Math.max(0, Math.min(itemBottom, viewportBottom) - Math.max(itemTop, viewportTop));
const visibilityRatio = itemHeight > 0 ? visibleHeight / itemHeight : 0;
const centerDistance = Math.abs(itemCenter - viewportCenter);
const topDistance = Math.abs(itemTop - viewportTop);
if (topDistance <= topProximityThreshold && visibilityRatio > .1) {
if (!topAlignedCandidate || topDistance < topAlignedCandidate.distance) topAlignedCandidate = {
index,
distance: topDistance
};
}
if (visibilityRatio > .1) {
if (!highestVisibilityCandidate || visibilityRatio > highestVisibilityCandidate.ratio || visibilityRatio === highestVisibilityCandidate.ratio && centerDistance < highestVisibilityCandidate.centerDistance) highestVisibilityCandidate = {
index,
ratio: visibilityRatio,
centerDistance
};
}
if (!bestCandidate || centerDistance < bestCandidate.distance) bestCandidate = {
index,
distance: centerDistance
};
}
if (topAlignedCandidate) return topAlignedCandidate;
if (highestVisibilityCandidate) return {
index: highestVisibilityCandidate.index,
distance: 0
};
return bestCandidate;
}
};
function resolve(value) {
return typeof value === "function" ? value() : value;
}
function resolveOptional(value) {
return value === void 0 ? void 0 : resolve(value);
}
function toAccessor(value) {
return typeof value === "function" ? value : () => value;
}
function toRequiredAccessor(resolver, fallback) {
return () => {
return resolveOptional(resolver()) ?? fallback;
};
}
function toOptionalAccessor(resolver) {
return () => resolveOptional(resolver());
}
function useGalleryFocusTracker(options) {
const isEnabled = toAccessor(options.isEnabled);
const container = toAccessor(options.container);
const isScrolling = options.isScrolling;
const lastNavigationTrigger = options.lastNavigationTrigger;
const shouldTrack = () => {
return isEnabled() && (isScrolling() || lastNavigationTrigger() === "scroll");
};
const coordinator = new FocusCoordinator({
isEnabled: shouldTrack,
container,
activeIndex: () => galleryState.value.currentIndex,
...options.threshold !== void 0 && { threshold: options.threshold },
rootMargin: options.rootMargin ?? "0px",
onFocusChange: (index, source) => {
if (source === "auto" && index !== null) navigateToItem(index, "scroll", "auto-focus");
}
});
onCleanup(() => coordinator.cleanup());
const handleItemFocus = (index) => {
navigateToItem(index, "keyboard", "keyboard");
};
return {
focusedIndex: () => gallerySignals.focusedIndex,
registerItem: (index, element) => coordinator.registerItem(index, element),
handleItemFocus,
forceSync: () => coordinator.updateFocus(true)
};
}
function useGalleryItemScroll(containerRef, currentIndex, totalItems, options = {}) {
const containerAccessor = typeof containerRef === "function" ? containerRef : () => containerRef.current;
const enabled = toAccessor(options.enabled ?? true);
const behavior = toAccessor(options.behavior ?? "auto");
const block = toAccessor(options.block ?? "start");
const alignToCenter = toAccessor(options.alignToCenter ?? false);
const isScrolling = toAccessor(options.isScrolling ?? false);
const currentIndexAccessor = toAccessor(currentIndex);
const totalItemsAccessor = toAccessor(totalItems);
const itemsCache =  new Map();
const getCachedItem = (index, itemsRoot) => {
const cached = itemsCache.get(index)?.deref();
if (cached?.isConnected) return cached;
const element = itemsRoot.querySelectorAll("[data-xeg-role=\"gallery-item\"]")[index];
if (element) itemsCache.set(index, new WeakRef(element));
return element ?? null;
};
const scrollToItem = (index) => {
const container = containerAccessor();
if (!enabled() || !container || index < 0 || index >= totalItemsAccessor()) return;
const itemsRoot = container.querySelector("[data-xeg-role=\"items-list\"], [data-xeg-role=\"items-container\"]");
if (!itemsRoot) return;
const target = getCachedItem(index, itemsRoot);
if (target) {
options.onScrollStart?.();
target.scrollIntoView({
behavior: behavior(),
block: alignToCenter() ? "center" : block(),
inline: "nearest"
});
} else requestAnimationFrame(() => {
const retryTarget = getCachedItem(index, itemsRoot);
if (retryTarget) {
options.onScrollStart?.();
retryTarget.scrollIntoView({
behavior: behavior(),
block: alignToCenter() ? "center" : block(),
inline: "nearest"
});
}
});
};
createEffect(() => {
const index = currentIndexAccessor();
const container = containerAccessor();
const total = totalItemsAccessor();
if (!container || total <= 0) return;
if (untrack(enabled) && !untrack(isScrolling)) scrollToItem(index);
});
return {
scrollToItem,
scrollToCurrentItem: () => scrollToItem(currentIndexAccessor())
};
}
var SCROLL_IDLE_TIMEOUT = 250;
var PROGRAMMATIC_SCROLL_WINDOW = 100;
var LISTENER_CONTEXT_PREFIX = "useGalleryScroll";
function useGalleryScroll({ container, scrollTarget, onScrollEnd, enabled = true, programmaticScrollTimestamp }) {
const containerAccessor = toAccessor(container);
const scrollTargetAccessor = toAccessor(scrollTarget ?? containerAccessor);
const enabledAccessor = toAccessor(enabled);
const programmaticTimestampAccessor = toAccessor(programmaticScrollTimestamp ?? 0);
const isGalleryOpen = createMemo(() => galleryState.value.isOpen);
const [isScrolling, setIsScrolling] = createSignal(false);
const [lastScrollTime, setLastScrollTime] = createSignal(0);
let scrollIdleTimerId = null;
const clearScrollIdleTimer = () => {
if (scrollIdleTimerId !== null) {
globalTimerManager.clearTimeout(scrollIdleTimerId);
scrollIdleTimerId = null;
}
};
const markScrolling = () => {
setIsScrolling(true);
setLastScrollTime(Date.now());
};
const scheduleScrollEnd = () => {
clearScrollIdleTimer();
scrollIdleTimerId = globalTimerManager.setTimeout(() => {
setIsScrolling(false);
onScrollEnd?.();
}, SCROLL_IDLE_TIMEOUT);
};
const shouldIgnoreScroll = () => Date.now() - programmaticTimestampAccessor() < PROGRAMMATIC_SCROLL_WINDOW;
const isToolbarScroll = (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return false;
return !!(target.closest("[data-gallery-element=\"toolbar\"]") || target.closest("[data-gallery-element=\"settings-panel\"]") || target.closest("[data-gallery-element=\"tweet-panel\"]") || target.closest("[data-role=\"toolbar\"]"));
};
const handleWheel = (event) => {
if (!isGalleryOpen() || !isGalleryInternalEvent(event)) return;
if (isToolbarScroll(event)) return;
markScrolling();
scheduleScrollEnd();
};
const handleScroll = () => {
if (!isGalleryOpen() || shouldIgnoreScroll()) return;
markScrolling();
scheduleScrollEnd();
};
createEffect(() => {
const isEnabled = enabledAccessor();
const containerElement = containerAccessor();
const eventTarget = scrollTargetAccessor() ?? containerElement;
if (!isEnabled || !eventTarget) {
setIsScrolling(false);
clearScrollIdleTimer();
return;
}
const eventManager = EventManager.getInstance();
const listenerContext = createPrefixedId(LISTENER_CONTEXT_PREFIX, ":");
const listenerIds = [];
const registerListener = (type, handler) => {
const id = eventManager.addEventListener(eventTarget, type, handler, {
passive: true,
context: listenerContext
});
if (id) listenerIds.push(id);
};
registerListener("wheel", handleWheel);
registerListener("scroll", handleScroll);
onCleanup(() => {
for (const id of listenerIds) eventManager.removeListener(id);
clearScrollIdleTimer();
setIsScrolling(false);
});
});
onCleanup(clearScrollIdleTimer);
return {
isScrolling,
lastScrollTime
};
}
function ensureGalleryScrollAvailable(element) {
if (!element) return;
element.querySelectorAll("[data-xeg-role=\"items-list\"], .itemsList, .content").forEach((el) => {
if (el.style.overflowY !== "auto" && el.style.overflowY !== "scroll") el.style.overflowY = "auto";
});
}
function computeViewportConstraints(rect, chrome = {}) {
const vw = Math.max(0, Math.floor(rect.width));
const vh = Math.max(0, Math.floor(rect.height));
const top = Math.max(0, Math.floor(chrome.paddingTop ?? 0));
const bottom = Math.max(0, Math.floor(chrome.paddingBottom ?? 0));
const toolbar = Math.max(0, Math.floor(chrome.toolbarHeight ?? 0));
return {
viewportW: vw,
viewportH: vh,
constrainedH: Math.max(0, vh - top - bottom - toolbar)
};
}
function applyViewportCssVars(el, v) {
el.style.setProperty("--xeg-viewport-w", `${v.viewportW}px`);
el.style.setProperty("--xeg-viewport-h", `${v.viewportH}px`);
el.style.setProperty("--xeg-viewport-height-constrained", `${v.constrainedH}px`);
}
function observeViewportCssVars(el, getChrome) {
let disposed = false;
const calcAndApply = () => {
if (disposed) return;
const rect = el.getBoundingClientRect();
applyViewportCssVars(el, computeViewportConstraints({
width: rect.width,
height: rect.height
}, getChrome()));
};
let pending = false;
const schedule = () => {
if (pending) return;
pending = true;
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => {
pending = false;
calcAndApply();
});
else globalTimerManager.setTimeout(() => {
pending = false;
calcAndApply();
}, 0);
};
calcAndApply();
let ro = null;
if (typeof ResizeObserver !== "undefined") {
ro = new ResizeObserver(() => schedule());
try {
ro.observe(el);
} catch {}
}
const onResize = () => schedule();
let resizeListenerId = null;
if (typeof window !== "undefined") resizeListenerId = EventManager.getInstance().addEventListener(window, "resize", createEventListener(onResize), {
passive: true,
context: "viewport:resize"
});
return () => {
disposed = true;
if (ro) try {
ro.disconnect();
} catch {}
if (resizeListenerId) {
EventManager.getInstance().removeListener(resizeListenerId);
resizeListenerId = null;
}
};
}
var ANIMATION_CLASSES = {
FADE_IN: "xeg-fade-in",
FADE_OUT: "xeg-fade-out"
};
function runCssAnimation(element, className) {
return new Promise((resolve) => {
try {
const cleanup = () => {
element.removeEventListener("animationend", cleanup);
element.classList.remove(className);
resolve();
};
element.addEventListener("animationend", cleanup, { once: true });
element.classList.add(className);
} catch {
resolve();
}
});
}
async function animateGalleryEnter(element) {
return runCssAnimation(element, ANIMATION_CLASSES.FADE_IN);
}
async function animateGalleryExit(element) {
return runCssAnimation(element, ANIMATION_CLASSES.FADE_OUT);
}
function useGalleryLifecycle(options) {
const { containerEl, toolbarWrapperEl, isVisible } = options;
createEffect(on(containerEl, (element) => {
if (element) ensureGalleryScrollAvailable(element);
}));
createEffect(on([containerEl, isVisible], ([container, visible]) => {
if (!container) return;
if (visible) animateGalleryEnter(container);
else {
animateGalleryExit(container);
const logCleanupFailure = (error) => {};
container.querySelectorAll("video").forEach((video) => {
try {
video.pause();
} catch (error) {
logCleanupFailure(error);
}
try {
if (video.currentTime !== 0) video.currentTime = 0;
} catch (error) {
logCleanupFailure(error);
}
});
}
}, { defer: true }));
createEffect(() => {
const container = containerEl();
const wrapper = toolbarWrapperEl();
if (!container || !wrapper) return;
const cleanup = observeViewportCssVars(container, () => {
return {
toolbarHeight: wrapper ? Math.floor(wrapper.getBoundingClientRect().height) : 0,
paddingTop: 0,
paddingBottom: 0
};
});
onCleanup(() => cleanup?.());
});
}
function registerNavigationEvents({ onTriggerChange, onNavigateComplete }) {
const stopStart = galleryIndexEvents.on("navigate:start", () => {});
const stopComplete = galleryIndexEvents.on("navigate:complete", (payload) => {
onTriggerChange(payload.trigger);
onNavigateComplete(payload);
});
return () => {
stopStart();
stopComplete();
};
}
function useGalleryNavigation(options) {
const { isVisible, scrollToItem } = options;
const [lastNavigationTrigger, setLastNavigationTrigger] = createSignal(null);
const [programmaticScrollTimestamp, setProgrammaticScrollTimestamp] = createSignal(0);
createEffect(on(isVisible, (visible) => {
if (!visible) return;
onCleanup(registerNavigationEvents({
onTriggerChange: setLastNavigationTrigger,
onNavigateComplete: ({ index, trigger }) => {
if (trigger === "scroll") return;
scrollToItem(index);
}
}));
}));
return {
lastNavigationTrigger,
setLastNavigationTrigger,
programmaticScrollTimestamp,
setProgrammaticScrollTimestamp
};
}
function useToolbarAutoHide(options) {
const { isVisible, hasItems } = options;
const computeInitialVisibility = () => !!(isVisible() && hasItems());
const [isInitialToolbarVisible, setIsInitialToolbarVisible] = createSignal(computeInitialVisibility());
let activeTimer = null;
const clearActiveTimer = () => {
if (activeTimer === null) return;
globalTimerManager.clearTimeout(activeTimer);
activeTimer = null;
};
createEffect(() => {
onCleanup(clearActiveTimer);
if (!computeInitialVisibility()) {
setIsInitialToolbarVisible(false);
return;
}
setIsInitialToolbarVisible(true);
const rawAutoHideDelay = getTypedSettingOr("toolbar.autoHideDelay", 3e3);
const autoHideDelay = Math.max(0, typeof rawAutoHideDelay === "number" ? rawAutoHideDelay : 0);
if (autoHideDelay === 0) {
setIsInitialToolbarVisible(false);
return;
}
activeTimer = globalTimerManager.setTimeout(() => {
setIsInitialToolbarVisible(false);
activeTimer = null;
}, autoHideDelay);
});
return {
isInitialToolbarVisible,
setIsInitialToolbarVisible
};
}
function useVerticalGallery(options) {
const { isVisible, currentIndex, mediaItemsCount, containerEl, toolbarWrapperEl, itemsContainerEl } = options;
let focusSyncCallback = null;
const { isInitialToolbarVisible, setIsInitialToolbarVisible } = useToolbarAutoHide({
isVisible,
hasItems: () => mediaItemsCount() > 0
});
let scrollToItemRef = null;
const navigationState = useGalleryNavigation({
isVisible,
scrollToItem: (index) => scrollToItemRef?.(index)
});
const { isScrolling } = useGalleryScroll({
container: containerEl,
scrollTarget: itemsContainerEl,
enabled: isVisible,
programmaticScrollTimestamp: () => navigationState.programmaticScrollTimestamp(),
onScrollEnd: () => focusSyncCallback?.()
});
const { scrollToItem, scrollToCurrentItem } = useGalleryItemScroll(containerEl, currentIndex, mediaItemsCount, {
enabled: () => !isScrolling() && navigationState.lastNavigationTrigger() !== "scroll",
block: "start",
isScrolling,
onScrollStart: () => navigationState.setProgrammaticScrollTimestamp(Date.now())
});
scrollToItemRef = scrollToItem;
const { focusedIndex, registerItem: registerFocusItem, handleItemFocus, forceSync: focusTrackerForceSync } = useGalleryFocusTracker({
container: containerEl,
isEnabled: isVisible,
isScrolling,
lastNavigationTrigger: navigationState.lastNavigationTrigger
});
focusSyncCallback = focusTrackerForceSync;
useGalleryLifecycle({
containerEl,
toolbarWrapperEl,
isVisible
});
createEffect(() => {
if (isScrolling()) setIsInitialToolbarVisible(false);
});
return {
scroll: {
isScrolling,
scrollToItem,
scrollToCurrentItem
},
navigation: {
lastNavigationTrigger: navigationState.lastNavigationTrigger,
setLastNavigationTrigger: navigationState.setLastNavigationTrigger,
programmaticScrollTimestamp: navigationState.programmaticScrollTimestamp,
setProgrammaticScrollTimestamp: navigationState.setProgrammaticScrollTimestamp
},
focus: {
focusedIndex,
registerItem: registerFocusItem,
handleItemFocus,
forceSync: focusTrackerForceSync
},
toolbar: {
isInitialToolbarVisible,
setIsInitialToolbarVisible
}
};
}
var VerticalGalleryView_module_default = {
container: "xg-X9gZ",
toolbarWrapper: "xg-meO3",
uiHidden: "xg-9abg",
isScrolling: "xg-sOsS",
itemsContainer: "xg-gmRW",
empty: "xg-yhK-",
galleryItem: "xg-EfVa",
itemActive: "xg-LxHL",
scrollSpacer: "xg-sfF0",
toolbarHoverZone: "xg-gC-m",
initialToolbarVisible: "xg-Canm",
toolbarButton: "xg-e06X",
emptyMessage: "xg-fwsr"
};
function createVideoVisibilityController(options) {
const { video, setMuted } = options;
let wasPlayingBeforeHidden = false;
let wasMutedBeforeHidden = null;
let didAutoMute = false;
const pauseVideo = () => {
if (typeof video.pause === "function") video.pause();
};
const playVideo = () => {
if (typeof video.play !== "function") return;
try {
const result = video.play();
if (result && typeof result.catch === "function") result.catch((err) => {});
} catch (err) {}
};
const applyMuted = (nextMuted) => {
if (typeof setMuted === "function") {
setMuted(video, nextMuted);
return;
}
video.muted = nextMuted;
};
return { handleEntry(entry) {
if (!entry.isIntersecting) try {
if (wasMutedBeforeHidden === null) {
wasPlayingBeforeHidden = !video.paused;
wasMutedBeforeHidden = video.muted;
didAutoMute = false;
}
if (!video.muted) {
applyMuted(true);
didAutoMute = true;
}
if (!video.paused) pauseVideo();
} catch (err) {}
else try {
if (wasMutedBeforeHidden !== null) {
if (didAutoMute && video.muted === true && wasMutedBeforeHidden === false) applyMuted(false);
}
if (wasPlayingBeforeHidden) playVideo();
} catch (err) {} finally {
wasPlayingBeforeHidden = false;
wasMutedBeforeHidden = null;
didAutoMute = false;
}
} };
}
function useVideoVisibility(options) {
const { container, video, isVideo, setMuted } = options;
createEffect(() => {
if (!isVideo()) return;
const containerEl = container();
const videoEl = video();
if (!containerEl || !videoEl) return;
const controller = createVideoVisibilityController(typeof setMuted === "function" ? {
video: videoEl,
setMuted
} : { video: videoEl });
const unsubscribeObserver = SharedObserver.observe(containerEl, controller.handleEntry, {
threshold: 0,
rootMargin: "0px"
});
onCleanup(() => {
unsubscribeObserver();
});
});
}
var DEFAULT_VOLUME_EPSILON = .001;
function areVolumesEquivalent(a, b) {
if (!Number.isFinite(a) || !Number.isFinite(b)) return a === b;
return Math.abs(a - b) <= DEFAULT_VOLUME_EPSILON;
}
function nowMs() {
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
}
function createVideoVolumeChangeGuard(options = {}) {
const windowMsInput = options.windowMs;
const windowMs = typeof windowMsInput === "number" && Number.isFinite(windowMsInput) ? Math.max(0, windowMsInput) : 500;
const MAX_EXPECTED_MARKS = 4;
let expectedMarks = [];
const pruneExpiredMarks = (now) => {
if (expectedMarks.length === 0) return;
expectedMarks = expectedMarks.filter((mark) => {
const age = now - mark.markedAt;
if (age < 0) return false;
return age <= windowMs;
});
};
return {
markProgrammaticChange(expected) {
const now = nowMs();
pruneExpiredMarks(now);
expectedMarks = [...expectedMarks, {
snapshot: expected,
markedAt: now
}];
if (expectedMarks.length > MAX_EXPECTED_MARKS) expectedMarks = expectedMarks.slice(-MAX_EXPECTED_MARKS);
},
shouldIgnoreChange(current) {
if (expectedMarks.length === 0) return false;
pruneExpiredMarks(nowMs());
if (expectedMarks.length === 0) return false;
return expectedMarks.some((mark) => areVolumesEquivalent(current.volume, mark.snapshot.volume) && current.muted === mark.snapshot.muted);
}
};
}
var VIDEO_EXTENSIONS = [
".mp4",
".webm",
".mov",
".avi"
];
var CLEAN_FILENAME_MAX_LENGTH = 40;
var CLEAN_FILENAME_TRUNCATION_MARKER = "...";
var CLEAN_FILENAME_EXTENSION_REGEX = /(?:\.[^./\\]{1,10})$/;
var CLEAN_FILENAME_TWITTER_PREFIX_REGEX = /^twitter_media_\d{8}T\d{6}_/;
var CLEAN_FILENAME_MEDIA_PREFIX_REGEX = /^\/media\//;
var CLEAN_FILENAME_RELATIVE_PREFIX_REGEX = /^\.\//;
function cleanFilename(filename) {
if (!filename) return "Untitled";
const truncateMiddlePreservingExtension = (value) => {
if (value.length <= CLEAN_FILENAME_MAX_LENGTH) return value;
const extension = value.match(CLEAN_FILENAME_EXTENSION_REGEX)?.[0] ?? "";
const base = extension ? value.slice(0, -extension.length) : value;
const available = CLEAN_FILENAME_MAX_LENGTH - extension.length - 3;
if (available <= 1) return value.slice(0, CLEAN_FILENAME_MAX_LENGTH);
const headLen = Math.max(1, Math.floor(available / 2));
const tailLen = Math.max(1, available - headLen);
return `${base.slice(0, headLen)}${CLEAN_FILENAME_TRUNCATION_MARKER}${base.slice(Math.max(0, base.length - tailLen))}${extension}`;
};
let cleaned = filename.replace(CLEAN_FILENAME_TWITTER_PREFIX_REGEX, "").replace(CLEAN_FILENAME_MEDIA_PREFIX_REGEX, "").replace(CLEAN_FILENAME_RELATIVE_PREFIX_REGEX, "");
const lastSegment = cleaned.split(/[\\/]/).pop();
if (lastSegment) cleaned = lastSegment;
if (/^[\\/]+$/.test(cleaned)) cleaned = "";
cleaned = cleaned.trim();
if (!cleaned) return "Image";
return truncateMiddlePreservingExtension(cleaned);
}
function normalizeVideoVolumeSetting(value, fallback = 1) {
const candidate = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
if (!Number.isFinite(candidate)) return fallback;
return Math.min(1, Math.max(0, candidate));
}
function normalizeVideoMutedSetting(value, fallback = false) {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1") return true;
if (normalized === "false" || normalized === "0") return false;
}
return fallback;
}
function isVideoMedia(media) {
const urlLowerCase = media.url.toLowerCase();
let parsedUrl = null;
try {
parsedUrl = new URL(media.url);
} catch {
parsedUrl = null;
}
const pathToCheck = parsedUrl ? parsedUrl.pathname.toLowerCase() : urlLowerCase.split(/[?#]/)[0] ?? "";
if (VIDEO_EXTENSIONS.some((ext) => pathToCheck.endsWith(ext))) return true;
if (media.filename) {
const filenameLowerCase = media.filename.toLowerCase();
if (VIDEO_EXTENSIONS.some((ext) => filenameLowerCase.endsWith(ext))) return true;
}
if (parsedUrl) return parsedUrl.hostname === "video.twimg.com";
return false;
}
function useVideoVolumePersistence(options) {
const { videoRef, isVideo } = options;
const [videoVolume, setVideoVolume] = createSignal(normalizeVideoVolumeSetting(getTypedSettingOr("gallery.videoVolume", 1), 1));
const [videoMuted, setVideoMuted] = createSignal(normalizeVideoMutedSetting(getTypedSettingOr("gallery.videoMuted", false), false));
let isApplyingVideoSettings = false;
const volumeChangeGuard = createVideoVolumeChangeGuard();
const applyMutedProgrammatically = (videoEl, muted) => {
volumeChangeGuard.markProgrammaticChange({
volume: videoEl.volume,
muted
});
videoEl.muted = muted;
};
const applyVolumeProgrammatically = (videoEl, volume) => {
volumeChangeGuard.markProgrammaticChange({
volume,
muted: videoEl.muted
});
videoEl.volume = volume;
};
createEffect(() => {
const video = videoRef();
if (video && isVideo()) {
isApplyingVideoSettings = true;
try {
untrack(() => {
const nextMuted = normalizeVideoMutedSetting(videoMuted(), false);
const nextVolume = normalizeVideoVolumeSetting(videoVolume(), 1);
if (nextMuted !== videoMuted()) setVideoMuted(nextMuted);
if (nextVolume !== videoVolume()) setVideoVolume(nextVolume);
applyMutedProgrammatically(video, nextMuted);
applyVolumeProgrammatically(video, nextVolume);
});
} finally {
isApplyingVideoSettings = false;
}
}
});
const debouncedSaveVolume = createDebounced((volume, muted) => {
setTypedSetting("gallery.videoVolume", volume);
setTypedSetting("gallery.videoMuted", muted);
}, 300);
onCleanup(() => {
debouncedSaveVolume.flush();
});
const handleVolumeChange = (event) => {
const video = event.currentTarget;
const snapshot = {
volume: video.volume,
muted: video.muted
};
if (isApplyingVideoSettings || volumeChangeGuard.shouldIgnoreChange(snapshot)) return;
const newVolume = normalizeVideoVolumeSetting(snapshot.volume, 1);
const newMuted = normalizeVideoMutedSetting(snapshot.muted, false);
setVideoVolume(newVolume);
setVideoMuted(newMuted);
debouncedSaveVolume(newVolume, newMuted);
};
return {
volumeChangeGuard,
applyMutedProgrammatically,
applyVolumeProgrammatically,
handleVolumeChange
};
}
var VerticalImageItem_module_default = {
container: "xg-huYo",
active: "xg-xm-1",
focused: "xg-luqi",
imageWrapper: "xg-8-c8",
placeholder: "xg-lhkE",
loadingSpinner: "xg-6YYD",
image: "xg-FWlk",
video: "xg-GUev",
loading: "xg-8Z3S",
loaded: "xg-y9iP",
fitOriginal: "xg-yYtG",
fitWidth: "xg-Uc0o",
fitHeight: "xg-M9Z6",
fitContainer: "xg--Mlr",
errorIcon: "xg-Wno7",
errorText: "xg-8-wi",
error: "xg-Gswe"
};
function useTranslation() {
const languageService = getLanguageService();
const [revision, setRevision] = createSignal(0);
onCleanup(languageService.onLanguageChange(() => {
setRevision((value) => value + 1);
}));
return (key, params) => {
revision();
return languageService.translate(key, params);
};
}
var _tmpl$$9 =  template(`<div data-xeg-role=gallery-item data-xeg-gallery=true data-xeg-gallery-type=item data-xeg-gallery-version=2.0 data-xeg-component=vertical-image-item data-xeg-block-twitter=true><div data-xeg-role=media-wrapper>`), _tmpl$2$4 =  template(`<div><div>`), _tmpl$3$1 =  template(`<video controls>`), _tmpl$4$1 =  template(`<img decoding=async>`, true, false, false), _tmpl$5 =  template(`<div><span>⚠️</span><span>`);
var FIT_MODE_CLASSES = {
original: VerticalImageItem_module_default.fitOriginal,
fitHeight: VerticalImageItem_module_default.fitHeight,
fitWidth: VerticalImageItem_module_default.fitWidth,
fitContainer: VerticalImageItem_module_default.fitContainer
};
function VerticalImageItem(props) {
const [local] = splitProps(props, [
"media",
"index",
"isActive",
"isFocused",
"forceVisible",
"onClick",
"onImageContextMenu",
"className",
"onMediaLoad",
"fitMode",
"style",
"data-testid",
"aria-label",
"aria-describedby",
"registerContainer",
"role",
"tabIndex",
"onFocus",
"onBlur",
"onKeyDown"
]);
const isFocused = createMemo(() => local.isFocused ?? false);
const className$1 = createMemo(() => local.className ?? "");
const shouldEagerLoad = createMemo(() => {
return (local.forceVisible ?? false) || (local.isActive ?? false);
});
const translate = useTranslation();
const isVideo = createMemo(() => {
switch (local.media.type) {
case "video":
case "gif": return true;
case "image": return false;
default: return isVideoMedia(local.media);
}
});
const [isLoaded, setIsLoaded] = createSignal(false);
const [isError, setIsError] = createSignal(false);
createEffect(() => {
setIsLoaded(false);
setIsError(false);
});
const [containerRef, setContainerRef] = createSignal(null);
const [imageRef, setImageRef] = createSignal(null);
const [videoRef, setVideoRef] = createSignal(null);
const resolvedDimensions = createMemo(() => resolveMediaDimensionsWithIntrinsicFlag(local.media));
const dimensions = () => resolvedDimensions().dimensions;
const hasIntrinsicSize = () => resolvedDimensions().hasIntrinsicSize;
const intrinsicSizingStyle = createMemo(() => {
return createIntrinsicSizingStyle(dimensions());
});
const mergedStyle = createMemo(() => {
const base = intrinsicSizingStyle();
const extra = local.style ?? {};
return {
...base,
...extra
};
});
const { applyMutedProgrammatically, handleVolumeChange } = useVideoVolumePersistence({
videoRef,
isVideo
});
useVideoVisibility({
container: containerRef,
video: videoRef,
isVideo,
setMuted: applyMutedProgrammatically
});
createEffect(() => {
const video = videoRef();
if (local.isActive && video) {
if (!untrack(() => gallerySignals.currentVideoElement === video)) setCurrentVideoElement(video);
return;
}
if (untrack(() => gallerySignals.currentVideoElement === video)) setCurrentVideoElement(null);
});
const preventDragStart = (event) => {
event.preventDefault();
};
const handleContainerClick = (event) => {
event.stopPropagation();
if (isVideo()) {
const video = videoRef();
if (video) {
const target = event.target;
const targetInVideo = target instanceof Node && video.contains(target);
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
const pathIncludesVideo = Array.isArray(path) && path.includes(video);
if (targetInVideo || pathIncludesVideo) return;
}
containerRef()?.focus?.({ preventScroll: true });
local.onClick();
return;
}
containerRef()?.focus?.({ preventScroll: true });
local.onClick();
};
const handleContainerKeyDown = (event) => {
if (typeof local.onKeyDown === "function") {
local.onKeyDown(event);
return;
}
if (local.role !== void 0 && local.role !== "button") return;
const key = event.key;
if (key === "Enter" || key === "Space") {
event.preventDefault();
event.stopPropagation();
local.onClick();
}
};
const handleMediaLoad = () => {
if (!isLoaded()) {
setIsLoaded(true);
setIsError(false);
local.onMediaLoad?.(local.media.id, local.index);
}
};
const handleMediaError = () => {
setIsError(true);
setIsLoaded(false);
};
const handleContextMenu = (event) => {
local.onImageContextMenu?.(event, local.media);
};
createEffect(() => {
if (isLoaded()) return;
if (isVideo()) {
const video = videoRef();
if (video && video.readyState >= 1) handleMediaLoad();
} else {
const image = imageRef();
if (image?.complete) if (image.naturalWidth > 0) handleMediaLoad();
else handleMediaError();
}
});
const resolvedFitMode = createMemo(() => {
const value = local.fitMode;
if (typeof value === "function") return value() ?? "fitWidth";
return value ?? "fitWidth";
});
const fitModeClass = createMemo(() => FIT_MODE_CLASSES[resolvedFitMode()] ?? "");
const containerClasses = createMemo(() => cx("xeg-gallery", "xeg-gallery-item", "vertical-item", VerticalImageItem_module_default.container, local.isActive ? VerticalImageItem_module_default.active : void 0, isFocused() ? VerticalImageItem_module_default.focused : void 0, className$1()));
const assignContainerRef = (element) => {
setContainerRef(element);
local.registerContainer?.(element);
};
const defaultContainerRole = createMemo(() => isVideo() ? "group" : "button");
const resolvedContainerRole = createMemo(() => local.role ?? defaultContainerRole());
const defaultAriaLabel = createMemo(() => translate("msg.gal.itemLbl", {
index: local.index + 1,
filename: cleanFilename(local.media.filename)
}));
return (() => {
var _el$ = _tmpl$$9(), _el$2 = _el$.firstChild;
_el$.$$keydown = handleContainerKeyDown;
addEventListener(_el$, "blur", local.onBlur);
addEventListener(_el$, "focus", local.onFocus);
_el$.$$click = handleContainerClick;
use(assignContainerRef, _el$);
insert(_el$2, (() => {
var _c$ = memo(() => !!(!isLoaded() && !isError()));
return () => _c$() && (() => {
var _el$3 = _tmpl$2$4(), _el$4 = _el$3.firstChild;
createRenderEffect((_p$) => {
var _v$11 = VerticalImageItem_module_default.placeholder, _v$12 = cx("xeg-spinner", VerticalImageItem_module_default.loadingSpinner);
_v$11 !== _p$.e && className(_el$3, _p$.e = _v$11);
_v$12 !== _p$.t && className(_el$4, _p$.t = _v$12);
return _p$;
}, {
e: void 0,
t: void 0
});
return _el$3;
})();
})(), null);
insert(_el$2, (() => {
var _c$2 = memo(() => !!isVideo());
return () => _c$2() ? (() => {
var _el$5 = _tmpl$3$1();
addEventListener(_el$5, "volumechange", handleVolumeChange);
_el$5.addEventListener("dragstart", preventDragStart);
_el$5.$$contextmenu = handleContextMenu;
_el$5.addEventListener("error", handleMediaError);
_el$5.addEventListener("canplay", handleMediaLoad);
_el$5.addEventListener("loadeddata", handleMediaLoad);
_el$5.addEventListener("loadedmetadata", handleMediaLoad);
use(setVideoRef, _el$5);
createRenderEffect((_p$) => {
var _v$13 = local.media.url, _v$14 = cx(VerticalImageItem_module_default.video, fitModeClass(), isLoaded() ? VerticalImageItem_module_default.loaded : VerticalImageItem_module_default.loading);
_v$13 !== _p$.e && setAttribute(_el$5, "src", _p$.e = _v$13);
_v$14 !== _p$.t && className(_el$5, _p$.t = _v$14);
return _p$;
}, {
e: void 0,
t: void 0
});
return _el$5;
})() : (() => {
var _el$6 = _tmpl$4$1();
_el$6.addEventListener("dragstart", preventDragStart);
_el$6.$$contextmenu = handleContextMenu;
_el$6.addEventListener("error", handleMediaError);
_el$6.addEventListener("load", handleMediaLoad);
use(setImageRef, _el$6);
createRenderEffect((_p$) => {
var _v$15 = local.media.url, _v$16 = cleanFilename(local.media.filename), _v$17 = shouldEagerLoad() ? "eager" : "lazy", _v$18 = cx(VerticalImageItem_module_default.image, fitModeClass(), isLoaded() ? VerticalImageItem_module_default.loaded : VerticalImageItem_module_default.loading);
_v$15 !== _p$.e && setAttribute(_el$6, "src", _p$.e = _v$15);
_v$16 !== _p$.t && setAttribute(_el$6, "alt", _p$.t = _v$16);
_v$17 !== _p$.a && setAttribute(_el$6, "loading", _p$.a = _v$17);
_v$18 !== _p$.o && className(_el$6, _p$.o = _v$18);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0
});
return _el$6;
})();
})(), null);
insert(_el$2, (() => {
var _c$3 = memo(() => !!isError());
return () => _c$3() && (() => {
var _el$7 = _tmpl$5(), _el$8 = _el$7.firstChild, _el$9 = _el$8.nextSibling;
insert(_el$9, () => translate("msg.gal.loadFail", { type: isVideo() ? "video" : "image" }));
createRenderEffect((_p$) => {
var _v$19 = VerticalImageItem_module_default.error, _v$20 = VerticalImageItem_module_default.errorIcon, _v$21 = VerticalImageItem_module_default.errorText;
_v$19 !== _p$.e && className(_el$7, _p$.e = _v$19);
_v$20 !== _p$.t && className(_el$8, _p$.t = _v$20);
_v$21 !== _p$.a && className(_el$9, _p$.a = _v$21);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0
});
return _el$7;
})();
})(), null);
createRenderEffect((_p$) => {
var _v$ = containerClasses(), _v$2 = local.index, _v$3 = resolvedFitMode(), _v$4 = isLoaded() ? "true" : "false", _v$5 = hasIntrinsicSize() ? "true" : void 0, _v$6 = mergedStyle(), _v$7 = local["aria-label"] || defaultAriaLabel(), _v$8 = local["aria-describedby"], _v$9 = resolvedContainerRole(), _v$0 = local.tabIndex ?? 0, _v$1 = void 0, _v$10 = VerticalImageItem_module_default.imageWrapper;
_v$ !== _p$.e && className(_el$, _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$, "data-index", _p$.t = _v$2);
_v$3 !== _p$.a && setAttribute(_el$, "data-fit-mode", _p$.a = _v$3);
_v$4 !== _p$.o && setAttribute(_el$, "data-media-loaded", _p$.o = _v$4);
_v$5 !== _p$.i && setAttribute(_el$, "data-has-intrinsic-size", _p$.i = _v$5);
_p$.n = style(_el$, _v$6, _p$.n);
_v$7 !== _p$.s && setAttribute(_el$, "aria-label", _p$.s = _v$7);
_v$8 !== _p$.h && setAttribute(_el$, "aria-describedby", _p$.h = _v$8);
_v$9 !== _p$.r && setAttribute(_el$, "role", _p$.r = _v$9);
_v$0 !== _p$.d && setAttribute(_el$, "tabindex", _p$.d = _v$0);
_v$1 !== _p$.l && setAttribute(_el$, "data-testid", _p$.l = _v$1);
_v$10 !== _p$.u && className(_el$2, _p$.u = _v$10);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0,
n: void 0,
s: void 0,
h: void 0,
r: void 0,
d: void 0,
l: void 0,
u: void 0
});
return _el$;
})();
}
delegateEvents([
"click",
"keydown",
"contextmenu"
]);
var _tmpl$$8 =  template(`<button>`);
function IconButton(props) {
return (() => {
var _el$ = _tmpl$$8();
addEventListener(_el$, "mousedown", props.onMouseDown, true);
addEventListener(_el$, "click", props.onClick, true);
var _ref$ = props.ref;
typeof _ref$ === "function" ? use(_ref$, _el$) : props.ref = _el$;
insert(_el$, () => props.children);
createRenderEffect((_p$) => {
var _v$ = props.id, _v$2 = props.type ?? "button", _v$3 = cx(props.class), _v$4 = props.title, _v$5 = props.disabled, _v$6 = props.tabIndex, _v$7 = props["data-testid"], _v$8 = props["aria-label"], _v$9 = props["aria-controls"], _v$0 = props["aria-expanded"], _v$1 = props["aria-pressed"], _v$10 = props["aria-busy"];
_v$ !== _p$.e && setAttribute(_el$, "id", _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$, "type", _p$.t = _v$2);
_v$3 !== _p$.a && className(_el$, _p$.a = _v$3);
_v$4 !== _p$.o && setAttribute(_el$, "title", _p$.o = _v$4);
_v$5 !== _p$.i && (_el$.disabled = _p$.i = _v$5);
_v$6 !== _p$.n && setAttribute(_el$, "tabindex", _p$.n = _v$6);
_v$7 !== _p$.s && setAttribute(_el$, "data-testid", _p$.s = _v$7);
_v$8 !== _p$.h && setAttribute(_el$, "aria-label", _p$.h = _v$8);
_v$9 !== _p$.r && setAttribute(_el$, "aria-controls", _p$.r = _v$9);
_v$0 !== _p$.d && setAttribute(_el$, "aria-expanded", _p$.d = _v$0);
_v$1 !== _p$.l && setAttribute(_el$, "aria-pressed", _p$.l = _v$1);
_v$10 !== _p$.u && setAttribute(_el$, "aria-busy", _p$.u = _v$10);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0,
n: void 0,
s: void 0,
h: void 0,
r: void 0,
d: void 0,
l: void 0,
u: void 0
});
return _el$;
})();
}
delegateEvents(["click", "mousedown"]);
var _tmpl$$7 =  template(`<svg xmlns=http://www.w3.org/2000/svg viewBox="0 0 24 24"fill=none stroke="var(--xeg-icon-color, currentColor)"stroke-width=var(--xeg-icon-stroke-width) stroke-linecap=round stroke-linejoin=round>`);
function Icon({ size = "var(--xeg-icon-size)", class: className = "", children, "aria-label": ariaLabel }) {
const sizeValue = typeof size === "number" ? `${size}px` : size;
return (() => {
var _el$ = _tmpl$$7();
setAttribute(_el$, "width", sizeValue);
setAttribute(_el$, "height", sizeValue);
setAttribute(_el$, "class", className);
setAttribute(_el$, "role", ariaLabel ? "img" : void 0);
setAttribute(_el$, "aria-label", ariaLabel);
setAttribute(_el$, "aria-hidden", !ariaLabel);
insert(_el$, children);
return _el$;
})();
}
var LUCIDE_ICON_NODES = {
"chevron-left": [["path", { d: "m15 18-6-6 6-6" }]],
"chevron-right": [["path", { d: "m9 18 6-6-6-6" }]],
download: [
["path", { d: "M12 15V3" }],
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
["path", { d: "m7 10 5 5 5-5" }]
],
"folder-down": [
["path", { d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" }],
["path", { d: "M12 10v6" }],
["path", { d: "m15 13-3 3-3-3" }]
],
"maximize-2": [
["path", { d: "M15 3h6v6" }],
["path", { d: "m21 3-7 7" }],
["path", { d: "m3 21 7-7" }],
["path", { d: "M9 21H3v-6" }]
],
"minimize-2": [
["path", { d: "m14 10 7-7" }],
["path", { d: "M20 10h-6V4" }],
["path", { d: "m3 21 7-7" }],
["path", { d: "M4 14h6v6" }]
],
"move-horizontal": [
["path", { d: "m18 8 4 4-4 4" }],
["path", { d: "M2 12h20" }],
["path", { d: "m6 8-4 4 4 4" }]
],
"move-vertical": [
["path", { d: "M12 2v20" }],
["path", { d: "m8 18 4 4 4-4" }],
["path", { d: "m8 6 4-4 4 4" }]
],
"settings-2": [
["path", { d: "M14 17H5" }],
["path", { d: "M19 7h-9" }],
["circle", {
cx: 17,
cy: 17,
r: 3
}],
["circle", {
cx: 7,
cy: 7,
r: 3
}]
],
"messages-square": [["path", { d: "M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" }], ["path", { d: "M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1" }]],
"external-link": [
["path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }],
["path", { d: "m15 3 6 6" }],
["path", { d: "M10 14 21 3" }]
],
x: [["path", { d: "M18 6 6 18" }], ["path", { d: "m6 6 12 12" }]]
};
var _tmpl$$6 =  template(`<svg><path></svg>`, false, true, false), _tmpl$2$3 =  template(`<svg><circle></svg>`, false, true, false);
var renderNode = (node) => {
const [tag, attrs] = node;
switch (tag) {
case "path": return (() => {
var _el$ = _tmpl$$6();
createRenderEffect(() => setAttribute(_el$, "d", attrs.d));
return _el$;
})();
case "circle": return (() => {
var _el$2 = _tmpl$2$3();
createRenderEffect((_p$) => {
var _v$ = attrs.cx, _v$2 = attrs.cy, _v$3 = attrs.r;
_v$ !== _p$.e && setAttribute(_el$2, "cx", _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$2, "cy", _p$.t = _v$2);
_v$3 !== _p$.a && setAttribute(_el$2, "r", _p$.a = _v$3);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0
});
return _el$2;
})();
default: return tag;
}
};
function LucideIcon(props) {
const nodes = LUCIDE_ICON_NODES[props.name];
return createComponent(Icon, {
get size() {
return props.size;
},
get ["class"]() {
return props.class;
},
get ["aria-label"]() {
return props["aria-label"];
},
get children() {
return nodes.map(renderNode);
}
});
}
var SettingsControls_module_default = {
body: "xg-EeSh",
bodyCompact: "xg-nm9B",
setting: "xg-PI5C",
settingCompact: "xg-VUTt",
label: "xg-vhT3",
compactLabel: "xg-Y62M",
select: "xg-jpiS"
};
var _tmpl$$5 =  template(`<div><div><label></label><select></select></div><div><label></label><select>`), _tmpl$2$2 =  template(`<option>`);
var THEME_OPTIONS = [
"auto",
"light",
"dark"
];
var LANGUAGE_OPTIONS = [
"auto",
"ko",
"en",
"ja"
];
function SettingsControls(props) {
const languageService = getLanguageService();
const [revision, setRevision] = createSignal(0);
onMount(() => {
onCleanup(languageService.onLanguageChange(() => setRevision((v) => v + 1)));
});
const strings = createMemo(() => {
revision();
return {
theme: {
title: languageService.translate("st.th"),
labels: {
auto: languageService.translate("st.thAuto"),
light: languageService.translate("st.thLt"),
dark: languageService.translate("st.thDk")
}
},
language: {
title: languageService.translate("st.lang"),
labels: {
auto: languageService.translate("st.langAuto"),
ko: languageService.translate("st.langKo"),
en: languageService.translate("st.langEn"),
ja: languageService.translate("st.langJa")
}
}
};
});
const themeValue = () => resolve(props.currentTheme);
const languageValue = () => resolve(props.currentLanguage);
return (() => {
var _el$ = _tmpl$$5(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling, _el$5 = _el$2.nextSibling, _el$6 = _el$5.firstChild, _el$7 = _el$6.nextSibling;
insert(_el$3, () => strings().theme.title);
addEventListener(_el$4, "change", props.onThemeChange);
insert(_el$4, () => THEME_OPTIONS.map((option) => (() => {
var _el$8 = _tmpl$2$2();
_el$8.value = option;
insert(_el$8, () => strings().theme.labels[option]);
return _el$8;
})()));
insert(_el$6, () => strings().language.title);
addEventListener(_el$7, "change", props.onLanguageChange);
insert(_el$7, () => LANGUAGE_OPTIONS.map((option) => (() => {
var _el$9 = _tmpl$2$2();
_el$9.value = option;
insert(_el$9, () => strings().language.labels[option]);
return _el$9;
})()));
createRenderEffect((_p$) => {
var _v$ = cx(SettingsControls_module_default.body, props.compact && SettingsControls_module_default.bodyCompact), _v$2 = void 0, _v$3 = cx(SettingsControls_module_default.setting, props.compact && SettingsControls_module_default.settingCompact), _v$4 = props["data-testid"] ? `${props["data-testid"]}-theme-select` : "settings-theme-select", _v$5 = cx(SettingsControls_module_default.label, props.compact && SettingsControls_module_default.compactLabel), _v$6 = props["data-testid"] ? `${props["data-testid"]}-theme-select` : "settings-theme-select", _v$7 = cx("xeg-inline-center", SettingsControls_module_default.select), _v$8 = strings().theme.title, _v$9 = strings().theme.title, _v$0 = void 0, _v$1 = cx(SettingsControls_module_default.setting, props.compact && SettingsControls_module_default.settingCompact), _v$10 = props["data-testid"] ? `${props["data-testid"]}-language-select` : "settings-language-select", _v$11 = cx(SettingsControls_module_default.label, props.compact && SettingsControls_module_default.compactLabel), _v$12 = props["data-testid"] ? `${props["data-testid"]}-language-select` : "settings-language-select", _v$13 = cx("xeg-inline-center", SettingsControls_module_default.select), _v$14 = strings().language.title, _v$15 = strings().language.title, _v$16 = void 0;
_v$ !== _p$.e && className(_el$, _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$, "data-testid", _p$.t = _v$2);
_v$3 !== _p$.a && className(_el$2, _p$.a = _v$3);
_v$4 !== _p$.o && setAttribute(_el$3, "for", _p$.o = _v$4);
_v$5 !== _p$.i && className(_el$3, _p$.i = _v$5);
_v$6 !== _p$.n && setAttribute(_el$4, "id", _p$.n = _v$6);
_v$7 !== _p$.s && className(_el$4, _p$.s = _v$7);
_v$8 !== _p$.h && setAttribute(_el$4, "aria-label", _p$.h = _v$8);
_v$9 !== _p$.r && setAttribute(_el$4, "title", _p$.r = _v$9);
_v$0 !== _p$.d && setAttribute(_el$4, "data-testid", _p$.d = _v$0);
_v$1 !== _p$.l && className(_el$5, _p$.l = _v$1);
_v$10 !== _p$.u && setAttribute(_el$6, "for", _p$.u = _v$10);
_v$11 !== _p$.c && className(_el$6, _p$.c = _v$11);
_v$12 !== _p$.w && setAttribute(_el$7, "id", _p$.w = _v$12);
_v$13 !== _p$.m && className(_el$7, _p$.m = _v$13);
_v$14 !== _p$.f && setAttribute(_el$7, "aria-label", _p$.f = _v$14);
_v$15 !== _p$.y && setAttribute(_el$7, "title", _p$.y = _v$15);
_v$16 !== _p$.g && setAttribute(_el$7, "data-testid", _p$.g = _v$16);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0,
n: void 0,
s: void 0,
h: void 0,
r: void 0,
d: void 0,
l: void 0,
u: void 0,
c: void 0,
w: void 0,
m: void 0,
f: void 0,
y: void 0,
g: void 0
});
createRenderEffect(() => _el$4.value = themeValue());
createRenderEffect(() => _el$7.value = languageValue());
return _el$;
})();
}
function findScrollableAncestor(target, scrollableSelector) {
if (!(target instanceof HTMLElement)) return null;
return target.closest(scrollableSelector);
}
function canConsumeWheelEvent(element, deltaY, tolerance = 1) {
const overflow = element.scrollHeight - element.clientHeight;
if (overflow <= tolerance) return false;
if (deltaY < 0) return element.scrollTop > tolerance;
if (deltaY > 0) return element.scrollTop < overflow - tolerance;
return true;
}
function shouldAllowWheelDefault$1(event, options) {
const scrollable = findScrollableAncestor(event.target, options.scrollableSelector);
if (!scrollable) return false;
return canConsumeWheelEvent(scrollable, event.deltaY, options.tolerance);
}
var Toolbar_module_default = {
toolbarButton: "xg-4eoj",
galleryToolbar: "xg-fLg7",
settingsExpanded: "xg-ZpP8",
tweetPanelExpanded: "xg-t4eq",
stateIdle: "xg-ojCW",
stateLoading: "xg-Y6KF",
stateDownloading: "xg-n-ab",
stateError: "xg-bEzl",
toolbarContent: "xg-f8g4",
toolbarControls: "xg-Ix3j",
counterBlock: "xg-0EHq",
separator: "xg-FKnO",
downloadCurrent: "xg-njlf",
downloadAll: "xg-AU-d",
closeButton: "xg-Vn14",
downloadButton: "xg-atmJ",
mediaCounterWrapper: "xg-GG86",
mediaCounter: "xg-2cjm",
currentIndex: "xg-JEXm",
totalCount: "xg-d1et",
progressBar: "xg-vB6N",
progressFill: "xg-LWQw",
fitButton: "xg-Q7dU",
settingsPanel: "xg-JcF-",
tweetPanel: "xg-yRtv",
panelExpanded: "xg-4a2L",
tweetPanelBody: "xg-w56C",
tweetTextHeader: "xg-rSWg",
tweetTextLabel: "xg-jd-V",
tweetContent: "xg-jmjG",
tweetUrlSection: "xg-0Eeq",
tweetUrlLink: "xg-AVKe",
tweetUrlIcon: "xg-5RjR",
tweetUrlLabel: "xg-8Stf",
tweetUrlValue: "xg-3pwZ",
tweetUrlDivider: "xg-sltl"
};
var _tmpl$$4 =  template(`<a target=_blank rel="noopener noreferrer">`), _tmpl$2$1 =  template(`<div><a target=_blank rel="noopener noreferrer"><span></span><span>`), _tmpl$3 =  template(`<div><div><span></div><div data-gallery-scrollable=true><span>`), _tmpl$4 =  template(`<div>`);
var TWEET_TEXT_URL_POLICY = {
allowedProtocols: new Set(["http:", "https:"]),
allowRelative: false,
allowProtocolRelative: false,
allowFragments: false,
allowDataUrls: false
};
var LINK_PATTERN = /https?:\/\/[^\s]+|(?<![\p{L}\p{N}_])#[\p{L}\p{N}_]+/gu;
var URL_TRAILING_PUNCTUATION = /[),.!?:;\]]+$/;
var PROTOCOL_PREFIX = /^https?:\/\//;
var buildHashtagUrl = (tag) => `https://x.com/hashtag/${encodeURIComponent(tag)}`;
var splitUrlTrailingPunctuation = (value) => {
const match = value.match(URL_TRAILING_PUNCTUATION);
if (!match) return {
url: value,
trailing: ""
};
const trailing = match[0] ?? "";
return {
url: value.slice(0, Math.max(0, value.length - trailing.length)),
trailing
};
};
var tokenizeTweetText = (input) => {
const tokens = [];
let lastIndex = 0;
for (const match of input.matchAll(LINK_PATTERN)) {
const startIndex = match.index ?? 0;
const rawMatch = match[0] ?? "";
if (startIndex > lastIndex) tokens.push({
type: "text",
value: input.slice(lastIndex, startIndex)
});
if (rawMatch.startsWith("http://") || rawMatch.startsWith("https://")) {
const { url, trailing } = splitUrlTrailingPunctuation(rawMatch);
if (url && isUrlAllowed(url, TWEET_TEXT_URL_POLICY)) {
tokens.push({
type: "url",
value: url,
href: url
});
if (trailing) tokens.push({
type: "text",
value: trailing
});
} else tokens.push({
type: "text",
value: rawMatch
});
} else if (rawMatch.startsWith("#")) {
const tag = rawMatch.slice(1);
if (tag) tokens.push({
type: "hashtag",
value: rawMatch,
href: buildHashtagUrl(tag)
});
else tokens.push({
type: "text",
value: rawMatch
});
} else tokens.push({
type: "text",
value: rawMatch
});
lastIndex = startIndex + rawMatch.length;
}
if (lastIndex < input.length) tokens.push({
type: "text",
value: input.slice(lastIndex)
});
return tokens;
};
var normalizeTweetUrl = (value) => {
const trimmed = value?.trim();
return trimmed && isUrlAllowed(trimmed, TWEET_TEXT_URL_POLICY) ? trimmed : null;
};
var formatTweetUrlLabel = (url) => url.replace(PROTOCOL_PREFIX, "");
var renderTweetTokens = (tokens) => tokens.map((token) => {
if ((token.type === "url" || token.type === "hashtag") && token.href) return (() => {
var _el$ = _tmpl$$4();
insert(_el$, () => token.value);
createRenderEffect(() => setAttribute(_el$, "href", token.href));
return _el$;
})();
return token.value;
});
function TweetUrlLink(props) {
const translate = useTranslation();
return (() => {
var _el$2 = _tmpl$2$1(), _el$3 = _el$2.firstChild, _el$4 = _el$3.firstChild, _el$5 = _el$4.nextSibling;
insert(_el$3, createComponent(LucideIcon, {
name: "external-link",
size: 14,
get ["class"]() {
return Toolbar_module_default.tweetUrlIcon;
}
}), _el$4);
insert(_el$4, () => translate("tb.twUrl"));
insert(_el$5, () => props.label);
createRenderEffect((_p$) => {
var _v$ = Toolbar_module_default.tweetUrlSection, _v$2 = props.url, _v$3 = Toolbar_module_default.tweetUrlLink, _v$4 = Toolbar_module_default.tweetUrlLabel, _v$5 = Toolbar_module_default.tweetUrlValue;
_v$ !== _p$.e && className(_el$2, _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$3, "href", _p$.t = _v$2);
_v$3 !== _p$.a && className(_el$3, _p$.a = _v$3);
_v$4 !== _p$.o && className(_el$4, _p$.o = _v$4);
_v$5 !== _p$.i && className(_el$5, _p$.i = _v$5);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0
});
return _el$2;
})();
}
function TweetTextPanel(props) {
const translate = useTranslation();
const tweetText = props.tweetTextHTML ?? props.tweetText ?? "";
const tokens = tweetText ? tokenizeTweetText(tweetText) : [];
const safeTweetUrl = normalizeTweetUrl(props.tweetUrl);
const tweetUrlLabel = safeTweetUrl ? formatTweetUrlLabel(safeTweetUrl) : "";
return (() => {
var _el$6 = _tmpl$3(), _el$7 = _el$6.firstChild, _el$8 = _el$7.firstChild, _el$9 = _el$7.nextSibling, _el$0 = _el$9.firstChild;
insert(_el$8, () => translate("tb.twTxt"));
insert(_el$9, safeTweetUrl && createComponent(TweetUrlLink, {
url: safeTweetUrl,
label: tweetUrlLabel
}), _el$0);
insert(_el$9, (() => {
var _c$ = memo(() => !!(safeTweetUrl && tokens.length > 0));
return () => _c$() && (() => {
var _el$1 = _tmpl$4();
createRenderEffect(() => className(_el$1, Toolbar_module_default.tweetUrlDivider));
return _el$1;
})();
})(), _el$0);
insert(_el$0, () => renderTweetTokens(tokens));
createRenderEffect((_p$) => {
var _v$6 = Toolbar_module_default.tweetPanelBody, _v$7 = Toolbar_module_default.tweetTextHeader, _v$8 = Toolbar_module_default.tweetTextLabel, _v$9 = Toolbar_module_default.tweetContent;
_v$6 !== _p$.e && className(_el$6, _p$.e = _v$6);
_v$7 !== _p$.t && className(_el$7, _p$.t = _v$7);
_v$8 !== _p$.a && className(_el$8, _p$.a = _v$8);
_v$9 !== _p$.o && className(_el$9, _p$.o = _v$9);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0
});
return _el$6;
})();
}
var _tmpl$$3 =  template(`<div data-gallery-element=toolbar><div><div><div><div><span aria-live=polite><span></span><span>/</span><span></span></span><div><div></div></div></div></div></div></div><div id=toolbar-settings-panel data-gallery-scrollable=true role=region aria-label="Settings Panel"aria-labelledby=settings-button data-gallery-element=settings-panel></div><div id=toolbar-tweet-panel role=region aria-labelledby=tweet-text-button data-gallery-element=tweet-panel>`);
var SCROLLABLE_SELECTOR = "[data-gallery-scrollable=\"true\"]";
var SCROLL_LOCK_TOLERANCE = 1;
var shouldAllowWheelDefault = (event) => {
return shouldAllowWheelDefault$1(event, {
scrollableSelector: SCROLLABLE_SELECTOR,
tolerance: SCROLL_LOCK_TOLERANCE
});
};
function ToolbarView(props) {
const totalCount = () => resolve(props.totalCount);
const currentIndex = () => resolve(props.currentIndex);
const isToolbarDisabled = () => !!resolveOptional(props.disabled);
const activeFitMode = () => props.activeFitMode();
const tweetText = () => resolveOptional(props.tweetText) ?? null;
const tweetTextHTML = () => resolveOptional(props.tweetTextHTML) ?? null;
const tweetUrl = () => resolveOptional(props.tweetUrl) ?? null;
const [toolbarElement, setToolbarElement] = createSignal(null);
const [counterElement, setCounterElement] = createSignal(null);
const [settingsPanelEl, setSettingsPanelEl] = createSignal(null);
const [tweetPanelEl, setTweetPanelEl] = createSignal(null);
const translate = useTranslation();
const nav = createMemo(() => props.navState());
const fitModeLabels = createMemo(() => resolve(props.fitModeLabels));
const assignToolbarRef = (element) => {
setToolbarElement(element);
props.settingsController.assignToolbarRef(element);
};
const assignSettingsPanelRef = (element) => {
setSettingsPanelEl(element);
props.settingsController.assignSettingsPanelRef(element);
};
createEffect(() => {
const toolbar = toolbarElement();
const counter = counterElement();
if (!toolbar && !counter) return;
const current = String(currentIndex());
const focused = String(props.displayedIndex());
if (toolbar) {
toolbar.dataset.currentIndex = current;
toolbar.dataset.focusedIndex = focused;
}
if (counter) {
counter.dataset.currentIndex = current;
counter.dataset.focusedIndex = focused;
}
});
const hasTweetContent = () => !!(tweetTextHTML() ?? tweetText() ?? tweetUrl());
const toolbarButtonClass = (...extra) => cx(Toolbar_module_default.toolbarButton, "xeg-inline-center", ...extra);
const toolbarStateClass = () => {
switch (props.toolbarDataState()) {
case "loading": return Toolbar_module_default.stateLoading;
case "downloading": return Toolbar_module_default.stateDownloading;
case "error": return Toolbar_module_default.stateError;
default: return Toolbar_module_default.stateIdle;
}
};
const handlePanelWheel = (event) => {
if (!shouldAllowWheelDefault(event)) return;
event.stopPropagation();
};
const preventScrollChaining = (event) => {
if (shouldAllowWheelDefault(event)) {
event.stopPropagation();
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
};
const registerWheelListener = (getElement, handler, options) => {
createEffect(() => {
const element = getElement();
if (!element) return;
const controller = new AbortController();
const eventManager = EventManager.getInstance();
const listener = (event) => handler(event);
eventManager.addEventListener(element, "wheel", listener, {
passive: options.passive,
signal: controller.signal,
context: options.context
});
onCleanup(() => controller.abort());
});
};
registerWheelListener(toolbarElement, preventScrollChaining, {
passive: false,
context: "toolbar:wheel:prevent-scroll-chaining"
});
registerWheelListener(settingsPanelEl, preventScrollChaining, {
passive: false,
context: "toolbar:wheel:prevent-scroll-chaining:settings-panel"
});
registerWheelListener(tweetPanelEl, handlePanelWheel, {
passive: true,
context: "toolbar:wheel:panel"
});
return (() => {
var _el$ = _tmpl$$3(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild, _el$4 = _el$3.firstChild, _el$5 = _el$4.firstChild, _el$6 = _el$5.firstChild, _el$7 = _el$6.firstChild, _el$8 = _el$7.nextSibling, _el$9 = _el$8.nextSibling, _el$0 = _el$6.nextSibling, _el$1 = _el$0.firstChild, _el$10 = _el$2.nextSibling, _el$11 = _el$10.nextSibling;
_el$.$$keydown = (event) => props.settingsController.handleToolbarKeyDown(event);
addEventListener(_el$, "blur", props.onBlur);
addEventListener(_el$, "focus", props.onFocus);
use(assignToolbarRef, _el$);
insert(_el$3, createComponent(IconButton, {
get ["class"]() {
return toolbarButtonClass();
},
size: "toolbar",
get ["aria-label"]() {
return translate("tb.prev");
},
get title() {
return translate("tb.prev");
},
get disabled() {
return nav().prevDisabled;
},
get onClick() {
return props.onPreviousClick;
},
get children() {
return createComponent(LucideIcon, {
name: "chevron-left",
size: 18
});
}
}), _el$4);
insert(_el$3, createComponent(IconButton, {
get ["class"]() {
return toolbarButtonClass();
},
size: "toolbar",
get ["aria-label"]() {
return translate("tb.next");
},
get title() {
return translate("tb.next");
},
get disabled() {
return nav().nextDisabled;
},
get onClick() {
return props.onNextClick;
},
get children() {
return createComponent(LucideIcon, {
name: "chevron-right",
size: 18
});
}
}), _el$4);
use((element) => {
setCounterElement(element);
}, _el$6);
insert(_el$7, () => props.displayedIndex() + 1);
insert(_el$9, totalCount);
insert(_el$3, () => props.fitModeOrder.map(({ mode, iconName }) => {
const label = fitModeLabels()[mode];
return createComponent(IconButton, {
get ["class"]() {
return toolbarButtonClass(Toolbar_module_default.fitButton);
},
size: "toolbar",
get onClick() {
return props.handleFitModeClick(mode);
},
get disabled() {
return props.isFitDisabled(mode);
},
get ["aria-label"]() {
return label.label;
},
get title() {
return label.title;
},
get ["aria-pressed"]() {
return activeFitMode() === mode;
},
get children() {
return createComponent(LucideIcon, {
name: iconName,
size: 18
});
}
});
}), null);
insert(_el$3, createComponent(IconButton, {
get ["class"]() {
return toolbarButtonClass(Toolbar_module_default.downloadButton, Toolbar_module_default.downloadCurrent);
},
size: "toolbar",
get onClick() {
return props.onDownloadCurrent;
},
get disabled() {
return nav().downloadDisabled;
},
get ["aria-label"]() {
return translate("tb.dl");
},
get title() {
return translate("tb.dl");
},
get children() {
return createComponent(LucideIcon, {
name: "download",
size: 18
});
}
}), null);
insert(_el$3, (() => {
var _c$ = memo(() => !!nav().canDownloadAll);
return () => _c$() && createComponent(IconButton, {
get ["class"]() {
return toolbarButtonClass(Toolbar_module_default.downloadButton, Toolbar_module_default.downloadAll);
},
size: "toolbar",
get onClick() {
return props.onDownloadAll;
},
get disabled() {
return nav().downloadDisabled;
},
get ["aria-label"]() {
return translate("tb.dlAllCt", { count: totalCount() });
},
get title() {
return translate("tb.dlAllCt", { count: totalCount() });
},
get children() {
return createComponent(LucideIcon, {
name: "folder-down",
size: 18
});
}
});
})(), null);
insert(_el$3, (() => {
var _c$2 = memo(() => !!props.showSettingsButton);
return () => _c$2() && createComponent(IconButton, {
ref(r$) {
var _ref$ = props.settingsController.assignSettingsButtonRef;
typeof _ref$ === "function" ? _ref$(r$) : props.settingsController.assignSettingsButtonRef = r$;
},
id: "settings-button",
get ["class"]() {
return toolbarButtonClass();
},
size: "toolbar",
get ["aria-label"]() {
return translate("tb.setOpen");
},
get ["aria-expanded"]() {
return props.settingsController.isSettingsExpanded() ? "true" : "false";
},
"aria-controls": "toolbar-settings-panel",
get title() {
return translate("tb.setOpen");
},
get disabled() {
return isToolbarDisabled();
},
get onMouseDown() {
return props.settingsController.handleSettingsMouseDown;
},
get onClick() {
return props.settingsController.handleSettingsClick;
},
get children() {
return createComponent(LucideIcon, {
name: "settings-2",
size: 18
});
}
});
})(), null);
insert(_el$3, (() => {
var _c$3 = memo(() => !!hasTweetContent());
return () => _c$3() && createComponent(IconButton, {
id: "tweet-text-button",
get ["class"]() {
return toolbarButtonClass();
},
size: "toolbar",
get ["aria-label"]() {
return translate("tb.twTxt");
},
get ["aria-expanded"]() {
return props.isTweetPanelExpanded() ? "true" : "false";
},
"aria-controls": "toolbar-tweet-panel",
get title() {
return translate("tb.twTxt");
},
get disabled() {
return isToolbarDisabled();
},
get onClick() {
return props.toggleTweetPanelExpanded;
},
get children() {
return createComponent(LucideIcon, {
name: "messages-square",
size: 18
});
}
});
})(), null);
insert(_el$3, createComponent(IconButton, {
get ["class"]() {
return toolbarButtonClass(Toolbar_module_default.closeButton);
},
size: "toolbar",
get ["aria-label"]() {
return translate("tb.cls");
},
get title() {
return translate("tb.cls");
},
get disabled() {
return isToolbarDisabled();
},
get onClick() {
return props.onCloseClick;
},
get children() {
return createComponent(LucideIcon, {
name: "x",
size: 18
});
}
}), null);
addEventListener(_el$10, "click", props.settingsController.handlePanelClick, true);
addEventListener(_el$10, "mousedown", props.settingsController.handlePanelMouseDown, true);
use(assignSettingsPanelRef, _el$10);
insert(_el$10, createComponent(Show, {
get when() {
return props.settingsController.isSettingsExpanded();
},
get children() {
return createComponent(SettingsControls, {
get currentTheme() {
return props.settingsController.currentTheme;
},
get currentLanguage() {
return props.settingsController.currentLanguage;
},
get onThemeChange() {
return props.settingsController.handleThemeChange;
},
get onLanguageChange() {
return props.settingsController.handleLanguageChange;
},
compact: true,
"data-testid": void 0
});
}
}));
use(setTweetPanelEl, _el$11);
insert(_el$11, createComponent(Show, {
get when() {
return memo(() => !!props.isTweetPanelExpanded())() && hasTweetContent();
},
get children() {
return createComponent(TweetTextPanel, {
get tweetText() {
return tweetText() ?? void 0;
},
get tweetTextHTML() {
return tweetTextHTML() ?? void 0;
},
get tweetUrl() {
return tweetUrl() ?? void 0;
}
});
}
}));
createRenderEffect((_p$) => {
var _v$ = cx(props.toolbarClass(), toolbarStateClass(), props.settingsController.isSettingsExpanded() ? Toolbar_module_default.settingsExpanded : void 0, props.isTweetPanelExpanded() ? Toolbar_module_default.tweetPanelExpanded : void 0), _v$2 = props.role ?? "toolbar", _v$3 = props["aria-label"] ?? "Gallery Toolbar", _v$4 = props["aria-describedby"], _v$5 = isToolbarDisabled(), _v$6 = void 0, _v$7 = props.tabIndex, _v$8 = cx(Toolbar_module_default.toolbarContent, "xeg-row-center"), _v$9 = Toolbar_module_default.toolbarControls, _v$0 = Toolbar_module_default.counterBlock, _v$1 = cx(Toolbar_module_default.mediaCounterWrapper, "xeg-inline-center"), _v$10 = cx(Toolbar_module_default.mediaCounter, "xeg-inline-center"), _v$11 = Toolbar_module_default.currentIndex, _v$12 = Toolbar_module_default.separator, _v$13 = Toolbar_module_default.totalCount, _v$14 = Toolbar_module_default.progressBar, _v$15 = Toolbar_module_default.progressFill, _v$16 = props.progressWidth(), _v$17 = cx(Toolbar_module_default.settingsPanel, props.settingsController.isSettingsExpanded() ? Toolbar_module_default.panelExpanded : void 0), _v$18 = cx(Toolbar_module_default.tweetPanel, props.isTweetPanelExpanded() ? Toolbar_module_default.panelExpanded : void 0), _v$19 = translate("tb.twPanel");
_v$ !== _p$.e && className(_el$, _p$.e = _v$);
_v$2 !== _p$.t && setAttribute(_el$, "role", _p$.t = _v$2);
_v$3 !== _p$.a && setAttribute(_el$, "aria-label", _p$.a = _v$3);
_v$4 !== _p$.o && setAttribute(_el$, "aria-describedby", _p$.o = _v$4);
_v$5 !== _p$.i && setAttribute(_el$, "aria-disabled", _p$.i = _v$5);
_v$6 !== _p$.n && setAttribute(_el$, "data-testid", _p$.n = _v$6);
_v$7 !== _p$.s && setAttribute(_el$, "tabindex", _p$.s = _v$7);
_v$8 !== _p$.h && className(_el$2, _p$.h = _v$8);
_v$9 !== _p$.r && className(_el$3, _p$.r = _v$9);
_v$0 !== _p$.d && className(_el$4, _p$.d = _v$0);
_v$1 !== _p$.l && className(_el$5, _p$.l = _v$1);
_v$10 !== _p$.u && className(_el$6, _p$.u = _v$10);
_v$11 !== _p$.c && className(_el$7, _p$.c = _v$11);
_v$12 !== _p$.w && className(_el$8, _p$.w = _v$12);
_v$13 !== _p$.m && className(_el$9, _p$.m = _v$13);
_v$14 !== _p$.f && className(_el$0, _p$.f = _v$14);
_v$15 !== _p$.y && className(_el$1, _p$.y = _v$15);
_v$16 !== _p$.g && setStyleProperty(_el$1, "width", _p$.g = _v$16);
_v$17 !== _p$.p && className(_el$10, _p$.p = _v$17);
_v$18 !== _p$.b && className(_el$11, _p$.b = _v$18);
_v$19 !== _p$.T && setAttribute(_el$11, "aria-label", _p$.T = _v$19);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0,
n: void 0,
s: void 0,
h: void 0,
r: void 0,
d: void 0,
l: void 0,
u: void 0,
c: void 0,
w: void 0,
m: void 0,
f: void 0,
y: void 0,
g: void 0,
p: void 0,
b: void 0,
T: void 0
});
return _el$;
})();
}
delegateEvents([
"keydown",
"mousedown",
"click"
]);
var toolbarSettingsControllerListenerSeq = 0;
var DEFAULTS = {
FOCUS_DELAY_MS: 50,
SELECT_GUARD_MS: 300
};
function useToolbarSettingsController(options) {
const { isSettingsExpanded, setSettingsExpanded, toggleSettingsExpanded, documentRef = typeof document !== "undefined" ? document : void 0, themeService: providedThemeService, languageService: providedLanguageService, focusDelayMs = DEFAULTS.FOCUS_DELAY_MS, selectChangeGuardMs = DEFAULTS.SELECT_GUARD_MS } = options;
const themeManager = providedThemeService ?? getThemeService();
const languageService = providedLanguageService ?? getLanguageService();
const scheduleTimeout = (callback, delay) => {
return globalTimerManager.setTimeout(callback, delay);
};
const clearScheduledTimeout = (handle) => {
if (handle == null) return;
globalTimerManager.clearTimeout(handle);
};
const [toolbarRef, setToolbarRef] = createSignal(void 0);
const [settingsPanelRef, setSettingsPanelRef] = createSignal(void 0);
const [settingsButtonRef, setSettingsButtonRef] = createSignal(void 0);
const toThemeOption = (value) => {
return value === "light" || value === "dark" ? value : "auto";
};
const getInitialTheme = () => {
try {
return toThemeOption(themeManager.getCurrentTheme());
} catch (error) {}
return "auto";
};
const [currentTheme, setCurrentTheme] = createSignal(getInitialTheme());
const [currentLanguage, setCurrentLanguage] = createSignal(languageService.getCurrentLanguage());
const syncThemeFromService = () => {
try {
setCurrentTheme(toThemeOption(themeManager.getCurrentTheme()));
} catch (error) {
;
}
};
syncThemeFromService();
if (typeof themeManager.isInitialized === "function" && !themeManager.isInitialized()) themeManager.initialize().then(syncThemeFromService).catch((error) => {
;
});
createEffect(() => {
const unsubscribe = themeManager.onThemeChange((_, setting) => {
setCurrentTheme(toThemeOption(setting));
});
onCleanup(() => {
unsubscribe?.();
});
});
createEffect(() => {
const unsubscribe = languageService.onLanguageChange((next) => {
setCurrentLanguage(next);
});
onCleanup(() => {
unsubscribe();
});
});
createEffect(() => {
if (!documentRef) return;
const expanded = isSettingsExpanded();
const panel = settingsPanelRef();
if (!expanded || !panel) return;
const eventManager = EventManager.getInstance();
const listenerContext = `toolbar-settings-controller:${toolbarSettingsControllerListenerSeq++}`;
let isSelectActive = false;
let selectGuardTimeout = null;
const handleSelectFocus = () => {
isSelectActive = true;
};
const handleSelectBlur = () => {
scheduleTimeout(() => {
isSelectActive = false;
}, 100);
};
const handleSelectChange = () => {
isSelectActive = true;
clearScheduledTimeout(selectGuardTimeout);
selectGuardTimeout = scheduleTimeout(() => {
isSelectActive = false;
selectGuardTimeout = null;
}, selectChangeGuardMs);
};
Array.from(panel.querySelectorAll("select")).forEach((select) => {
eventManager.addEventListener(select, "focus", handleSelectFocus, { context: listenerContext });
eventManager.addEventListener(select, "blur", handleSelectBlur, { context: listenerContext });
eventManager.addEventListener(select, "change", handleSelectChange, { context: listenerContext });
});
const handleOutsideClick = (event) => {
const target = event.target;
const settingsButton = settingsButtonRef();
const toolbarElement = toolbarRef();
if (!target) return;
if (isSelectActive) return;
const targetElement = target;
if (toolbarElement?.contains(targetElement)) return;
if (settingsButton?.contains(targetElement)) return;
if (panel.contains(targetElement)) return;
let currentNode = targetElement;
while (currentNode) {
if (currentNode.tagName === "SELECT" || currentNode.tagName === "OPTION") return;
currentNode = currentNode.parentElement;
}
setSettingsExpanded(false);
};
eventManager.addEventListener(documentRef, "mousedown", handleOutsideClick, {
capture: false,
context: listenerContext
});
onCleanup(() => {
clearScheduledTimeout(selectGuardTimeout);
eventManager.removeByContext(listenerContext);
});
});
const handleSettingsClick = (event) => {
event.stopImmediatePropagation?.();
const wasExpanded = isSettingsExpanded();
toggleSettingsExpanded();
if (!wasExpanded) scheduleTimeout(() => {
const firstControl = settingsPanelRef()?.querySelector("select");
if (firstControl) firstControl.focus({ preventScroll: true });
}, focusDelayMs);
};
const handleSettingsMouseDown = (event) => {
event.stopPropagation();
};
const handleToolbarKeyDown = (event) => {
if (event.key === "Escape" && isSettingsExpanded()) {
event.preventDefault();
event.stopPropagation();
setSettingsExpanded(false);
scheduleTimeout(() => {
const settingsButton = settingsButtonRef();
if (settingsButton) settingsButton.focus({ preventScroll: true });
}, focusDelayMs);
}
};
const handlePanelMouseDown = (event) => {
event.stopPropagation();
};
const handlePanelClick = (event) => {
event.stopPropagation();
};
const handleThemeChange = (event) => {
const select = event.target;
if (!select) return;
const theme = toThemeOption(select.value);
setCurrentTheme(theme);
themeManager.setTheme(theme);
try {
const settingsService = tryGetSettingsManager();
if (settingsService) settingsService.set("gallery.theme", theme).catch((error) => {
;
});
} catch (error) {
;
}
};
const handleLanguageChange = (event) => {
const select = event.target;
if (!select) return;
const language = select.value || "auto";
setCurrentLanguage(language);
languageService.setLanguage(language);
};
return {
assignToolbarRef: (element) => {
setToolbarRef(element ?? void 0);
},
assignSettingsPanelRef: (element) => {
setSettingsPanelRef(element ?? void 0);
},
assignSettingsButtonRef: (element) => {
setSettingsButtonRef(element ?? void 0);
},
isSettingsExpanded,
currentTheme,
currentLanguage,
handleSettingsClick,
handleSettingsMouseDown,
handleToolbarKeyDown,
handlePanelMouseDown,
handlePanelClick,
handleThemeChange,
handleLanguageChange
};
}
var DOWNLOAD_MIN_DISPLAY_TIME = 300;
var INITIAL_STATE = {
isDownloading: false,
isLoading: false,
hasError: false
};
function useToolbarState() {
const [isDownloading, setIsDownloading] = createSignal(INITIAL_STATE.isDownloading);
const [isLoading, setIsLoading] = createSignal(INITIAL_STATE.isLoading);
const [hasError, setHasError] = createSignal(INITIAL_STATE.hasError);
let lastDownloadToggle = 0;
let downloadTimeoutRef = null;
const clearDownloadTimeout = () => {
if (downloadTimeoutRef !== null) {
globalTimerManager.clearTimeout(downloadTimeoutRef);
downloadTimeoutRef = null;
}
};
const setDownloading = (downloading) => {
const now = Date.now();
if (downloading) {
lastDownloadToggle = now;
clearDownloadTimeout();
setIsDownloading(true);
setHasError(false);
return;
}
const timeSinceStart = now - lastDownloadToggle;
if (timeSinceStart < DOWNLOAD_MIN_DISPLAY_TIME) {
clearDownloadTimeout();
downloadTimeoutRef = globalTimerManager.setTimeout(() => {
setIsDownloading(false);
downloadTimeoutRef = null;
}, DOWNLOAD_MIN_DISPLAY_TIME - timeSinceStart);
return;
}
setIsDownloading(false);
};
const setLoading = (loading) => {
setIsLoading(loading);
if (loading) setHasError(false);
};
const setError = (errorState) => {
setHasError(errorState);
if (errorState) {
setIsLoading(false);
setIsDownloading(false);
}
};
const resetState = () => {
clearDownloadTimeout();
lastDownloadToggle = 0;
setIsDownloading(INITIAL_STATE.isDownloading);
setIsLoading(INITIAL_STATE.isLoading);
setHasError(INITIAL_STATE.hasError);
};
onCleanup(() => {
clearDownloadTimeout();
});
return [{
get isDownloading() {
return isDownloading();
},
get isLoading() {
return isLoading();
},
get hasError() {
return hasError();
}
}, {
setDownloading,
setLoading,
setError,
resetState
}];
}
var DEFAULT_PROPS = {
isDownloading: false,
disabled: false,
className: ""
};
var FIT_MODE_ORDER = [
{
mode: "original",
iconName: "maximize-2"
},
{
mode: "fitWidth",
iconName: "move-horizontal"
},
{
mode: "fitHeight",
iconName: "move-vertical"
},
{
mode: "fitContainer",
iconName: "minimize-2"
}
];
var computeNavigationState = ({ total, toolbarDisabled, downloadBusy }) => {
const hasItems = total > 0;
const canNavigate = hasItems && total > 1;
return {
prevDisabled: toolbarDisabled || !canNavigate,
nextDisabled: toolbarDisabled || !canNavigate,
canDownloadAll: total > 1,
downloadDisabled: toolbarDisabled || downloadBusy || !hasItems,
anyActionDisabled: toolbarDisabled
};
};
var createGuardedHandler = (guard, action) => {
return (event) => {
event.preventDefault();
event.stopPropagation();
if (guard()) return;
action?.();
};
};
function getToolbarDataState(state) {
if (state.hasError) return "error";
if (state.isDownloading) return "downloading";
if (state.isLoading) return "loading";
return "idle";
}
function ToolbarContainer(rawProps) {
const [local, domProps] = splitProps(mergeProps(DEFAULT_PROPS, rawProps), [
"currentIndex",
"totalCount",
"focusedIndex",
"isDownloading",
"disabled",
"className",
"currentFitMode",
"handlers",
"tweetText",
"tweetTextHTML",
"tweetUrl"
]);
const currentIndex = toRequiredAccessor(() => local.currentIndex, 0);
const totalCount = toRequiredAccessor(() => local.totalCount, 0);
const focusedIndex = toRequiredAccessor(() => local.focusedIndex, null);
const isDownloadingProp = toRequiredAccessor(() => local.isDownloading, false);
const isDisabled = toRequiredAccessor(() => local.disabled, false);
const currentFitMode = toOptionalAccessor(() => local.currentFitMode);
const tweetText = toOptionalAccessor(() => local.tweetText);
const tweetTextHTML = toOptionalAccessor(() => local.tweetTextHTML);
const tweetUrl = toOptionalAccessor(() => local.tweetUrl);
const translate = useTranslation();
const [toolbarState, toolbarActions] = useToolbarState();
const [settingsExpandedSignal, setSettingsExpandedSignal] = createSignal(false);
const [tweetExpanded, setTweetExpanded] = createSignal(false);
const toolbarClass = createMemo(() => cx(Toolbar_module_default.toolbar, Toolbar_module_default.galleryToolbar, local.className));
const totalItems = createMemo(() => Math.max(0, totalCount()));
const currentIndexForNav = createMemo(() => clampIndex(currentIndex(), totalItems()));
const displayedIndex = createMemo(() => {
const total = totalItems();
const currentIdx = currentIndexForNav();
const focusIdx = focusedIndex();
if (total <= 0) return 0;
if (typeof focusIdx === "number" && focusIdx >= 0 && focusIdx < total) return focusIdx;
return currentIdx;
});
const progressWidth = createMemo(() => {
const total = totalItems();
const idx = displayedIndex();
return total <= 0 ? "0%" : `${(idx + 1) / total * 100}%`;
});
const toolbarDataState = createMemo(() => getToolbarDataState(toolbarState));
const navState = createMemo(() => computeNavigationState({
total: totalItems(),
toolbarDisabled: !!isDisabled(),
downloadBusy: !!(isDownloadingProp() || toolbarState.isDownloading)
}));
const fitModeHandlers = createMemo(() => ({
original: local.handlers.fitMode?.onFitOriginal,
fitWidth: local.handlers.fitMode?.onFitWidth,
fitHeight: local.handlers.fitMode?.onFitHeight,
fitContainer: local.handlers.fitMode?.onFitContainer
}));
const fitModeLabels = createMemo(() => ({
original: {
label: translate("tb.fitOri"),
title: translate("tb.fitOri")
},
fitWidth: {
label: translate("tb.fitW"),
title: translate("tb.fitW")
},
fitHeight: {
label: translate("tb.fitH"),
title: translate("tb.fitH")
},
fitContainer: {
label: translate("tb.fitC"),
title: translate("tb.fitC")
}
}));
const activeFitMode = createMemo(() => currentFitMode() ?? FIT_MODE_ORDER[0]?.mode ?? "original");
createEffect(on(isDownloadingProp, (value) => {
toolbarActions.setDownloading(!!value);
}));
const setSettingsExpanded = (expanded) => {
setSettingsExpandedSignal(expanded);
if (expanded) setTweetExpanded(false);
};
const toggleSettings = () => setSettingsExpanded(!settingsExpandedSignal());
const toggleTweet = () => {
setTweetExpanded((prev) => {
if (!prev) setSettingsExpanded(false);
return !prev;
});
};
const isToolbarDisabled = () => !!isDisabled();
const isFitDisabled = (mode) => {
if (isToolbarDisabled()) return true;
if (!fitModeHandlers()[mode]) return true;
return activeFitMode() === mode;
};
const handleFitModeClick = (mode) => (event) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (!isToolbarDisabled()) fitModeHandlers()[mode]?.(event);
};
const handlePrevious = createGuardedHandler(() => navState().prevDisabled, local.handlers.navigation.onPrevious);
const handleNext = createGuardedHandler(() => navState().nextDisabled, local.handlers.navigation.onNext);
const handleDownloadCurrent = createGuardedHandler(() => navState().downloadDisabled, local.handlers.download.onDownloadCurrent);
const handleDownloadAll = createGuardedHandler(() => navState().downloadDisabled, local.handlers.download.onDownloadAll);
const handleClose = (event) => {
event.preventDefault();
event.stopPropagation();
local.handlers.lifecycle.onClose();
};
const baseSettingsController = useToolbarSettingsController({
isSettingsExpanded: settingsExpandedSignal,
setSettingsExpanded,
toggleSettingsExpanded: toggleSettings
});
const settingsController = {
...baseSettingsController,
handleSettingsClick: (event) => {
const wasOpen = settingsExpandedSignal();
baseSettingsController.handleSettingsClick(event);
if (!wasOpen && settingsExpandedSignal()) local.handlers.lifecycle.onOpenSettings?.();
}
};
return createComponent(ToolbarView, {
currentIndex,
focusedIndex,
totalCount,
isDownloading: isDownloadingProp,
disabled: isDisabled,
get ["aria-label"]() {
return domProps["aria-label"];
},
get ["aria-describedby"]() {
return domProps["aria-describedby"];
},
get role() {
return domProps.role;
},
get tabIndex() {
return domProps.tabIndex;
},
get ["data-testid"]() {
return domProps["data-testid"];
},
get onFocus() {
return local.handlers.focus?.onFocus;
},
get onBlur() {
return local.handlers.focus?.onBlur;
},
tweetText,
tweetTextHTML,
tweetUrl,
toolbarClass,
toolbarState,
toolbarDataState,
navState,
displayedIndex,
progressWidth,
fitModeOrder: FIT_MODE_ORDER,
fitModeLabels,
activeFitMode,
handleFitModeClick,
isFitDisabled,
onPreviousClick: handlePrevious,
onNextClick: handleNext,
onDownloadCurrent: handleDownloadCurrent,
onDownloadAll: handleDownloadAll,
onCloseClick: handleClose,
settingsController,
get showSettingsButton() {
return typeof local.handlers.lifecycle.onOpenSettings === "function";
},
isTweetPanelExpanded: tweetExpanded,
toggleTweetPanelExpanded: toggleTweet
});
}
var Toolbar = ToolbarContainer;
var [_isProcessing, setIsProcessing] = createSignal(false);
function acquireDownloadLock() {
if (_isProcessing()) return null;
setIsProcessing(true);
return () => {
setIsProcessing(false);
};
}
var downloadState = { get value() {
return { isProcessing: _isProcessing() };
} };
function computePreloadIndices(currentIndex, total, count) {
const safeTotal = Number.isFinite(total) && total > 0 ? Math.floor(total) : 0;
if (safeTotal === 0) return [];
const safeIndex = clampIndex(Math.floor(currentIndex), safeTotal);
const safeCount = clamp(Math.floor(count), 0, 20);
if (safeCount === 0) return [];
const indices = [];
for (let i = 1; i <= safeCount; i += 1) {
const idx = safeIndex - i;
if (idx >= 0) indices.push(idx);
else break;
}
for (let i = 1; i <= safeCount; i += 1) {
const idx = safeIndex + i;
if (idx < safeTotal) indices.push(idx);
else break;
}
return indices;
}
var _tmpl$$2 =  template(`<div><div><h3></h3><p>`), _tmpl$2 =  template(`<div data-xeg-gallery=true data-xeg-role=gallery><div data-role=toolbar-hover-zone></div><div data-role=toolbar></div><div data-xeg-role=items-container data-xeg-role-compat=items-list><div aria-hidden=true data-xeg-role=scroll-spacer>`);
function VerticalGalleryViewCore(props) {
const [local] = splitProps(props, [
"onClose",
"className",
"onPrevious",
"onNext",
"onDownloadCurrent",
"onDownloadAll"
]);
const handleClose = local.onClose ?? (() => {});
const mediaItems = createMemo(() => gallerySignals.mediaItems);
const currentIndex = createMemo(() => gallerySignals.currentIndex);
const isDownloading = createMemo(() => downloadState.value.isProcessing);
const [containerEl, setContainerEl] = createSignal(null);
const [toolbarWrapperEl, setToolbarWrapperEl] = createSignal(null);
const [itemsContainerEl, setItemsContainerEl] = createSignal(null);
const isVisible = createMemo(() => mediaItems().length > 0);
const activeMedia = createMemo(() => {
return mediaItems()[currentIndex()] ?? null;
});
const preloadIndices = createMemo(() => {
const count = getTypedSettingOr("gallery.preloadCount", 3);
return computePreloadIndices(currentIndex(), mediaItems().length, count);
});
const { scroll, navigation, focus, toolbar } = useVerticalGallery({
isVisible,
currentIndex,
mediaItemsCount: () => mediaItems().length,
containerEl,
toolbarWrapperEl,
itemsContainerEl
});
useGalleryKeyboard({ onClose: handleClose });
const translate = useTranslation();
const { debouncedScrollCorrection } = useGalleryScrollCorrection({
isVisible,
currentIndex,
activeMedia,
scrollToItem: scroll.scrollToItem
});
createEffect(() => {
if (!isVisible() || navigation.lastNavigationTrigger()) return;
navigateToItem(currentIndex(), "click", "auto-focus");
});
const { imageFitMode, handleFitOriginal, handleFitWidth, handleFitHeight, handleFitContainer } = useGalleryFitMode({
scrollToCurrentItem: scroll.scrollToCurrentItem,
currentIndex
});
const handleDownloadCurrent = () => local.onDownloadCurrent?.();
const handleDownloadAll = () => local.onDownloadAll?.();
const handleMediaLoad = (mediaId, indexValue) => debouncedScrollCorrection(indexValue, mediaId);
const createRegisterContainer = (index) => (element) => focus.registerItem(index, element);
const createHandleFocus = (index) => () => focus.handleItemFocus(index);
const { handlePrevious, handleNext, handleBackgroundClick, handleMediaItemClick } = useGalleryNavigationHandlers({
currentIndex,
mediaItems,
onClose: handleClose
});
useGalleryWheelRedirect({
containerEl,
itemsContainerEl
});
if (!isVisible()) return (() => {
var _el$ = _tmpl$$2(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling;
insert(_el$3, () => translate("msg.gal.emptyT"));
insert(_el$4, () => translate("msg.gal.emptyD"));
createRenderEffect((_p$) => {
var _v$ = cx(VerticalGalleryView_module_default.container, VerticalGalleryView_module_default.empty, local.className), _v$2 = VerticalGalleryView_module_default.emptyMessage;
_v$ !== _p$.e && className(_el$, _p$.e = _v$);
_v$2 !== _p$.t && className(_el$2, _p$.t = _v$2);
return _p$;
}, {
e: void 0,
t: void 0
});
return _el$;
})();
return (() => {
var _el$5 = _tmpl$2(), _el$6 = _el$5.firstChild, _el$7 = _el$6.nextSibling, _el$8 = _el$7.nextSibling, _el$9 = _el$8.firstChild;
addEventListener(_el$5, "click", handleBackgroundClick, true);
use((el) => setContainerEl(el ?? null), _el$5);
use((el) => setToolbarWrapperEl(el ?? null), _el$7);
insert(_el$7, createComponent(Toolbar, {
currentIndex,
get focusedIndex() {
return focus.focusedIndex;
},
totalCount: () => mediaItems().length,
isDownloading,
get currentFitMode() {
return imageFitMode();
},
tweetText: () => activeMedia()?.tweetText,
tweetTextHTML: () => activeMedia()?.tweetTextHTML,
tweetUrl: () => activeMedia()?.tweetUrl,
get className() {
return VerticalGalleryView_module_default.toolbar;
},
get handlers() {
return {
navigation: {
onPrevious: local.onPrevious ?? handlePrevious,
onNext: local.onNext ?? handleNext
},
download: {
onDownloadCurrent: handleDownloadCurrent,
onDownloadAll: handleDownloadAll
},
fitMode: {
onFitOriginal: handleFitOriginal,
onFitWidth: handleFitWidth,
onFitHeight: handleFitHeight,
onFitContainer: handleFitContainer
},
lifecycle: {
onClose: handleClose,
onOpenSettings: () => {}
}
};
}
}));
use((el) => setItemsContainerEl(el ?? null), _el$8);
insert(_el$8, createComponent(For, {
get each() {
return mediaItems();
},
children: (item, index) => {
const actualIndex = index();
return createComponent(VerticalImageItem, {
media: item,
index: actualIndex,
get isActive() {
return actualIndex === currentIndex();
},
get isFocused() {
return actualIndex === focus.focusedIndex();
},
forceVisible: preloadIndices().includes(actualIndex),
fitMode: imageFitMode,
onClick: () => handleMediaItemClick(actualIndex),
onMediaLoad: handleMediaLoad,
get className() {
return cx(VerticalGalleryView_module_default.galleryItem, actualIndex === currentIndex() && VerticalGalleryView_module_default.itemActive);
},
get registerContainer() {
return createRegisterContainer(actualIndex);
},
get onFocus() {
return createHandleFocus(actualIndex);
}
});
}
}), _el$9);
createRenderEffect((_p$) => {
var _v$3 = cx(VerticalGalleryView_module_default.container, toolbar.isInitialToolbarVisible() && VerticalGalleryView_module_default.initialToolbarVisible, scroll.isScrolling() && VerticalGalleryView_module_default.isScrolling, local.className), _v$4 = VerticalGalleryView_module_default.toolbarHoverZone, _v$5 = VerticalGalleryView_module_default.toolbarWrapper, _v$6 = VerticalGalleryView_module_default.itemsContainer, _v$7 = VerticalGalleryView_module_default.scrollSpacer;
_v$3 !== _p$.e && className(_el$5, _p$.e = _v$3);
_v$4 !== _p$.t && className(_el$6, _p$.t = _v$4);
_v$5 !== _p$.a && className(_el$7, _p$.a = _v$5);
_v$6 !== _p$.o && className(_el$8, _p$.o = _v$6);
_v$7 !== _p$.i && className(_el$9, _p$.i = _v$7);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0
});
return _el$5;
})();
}
var VerticalGalleryView = VerticalGalleryViewCore;
delegateEvents(["click"]);
function useGalleryDownload() {
const userscript = getUserscript();
const getDownloadErrorNotification = (error) => {
const message = normalizeErrorMessage(error);
try {
const languageService = getLanguageService();
return {
title: languageService.translate("msg.dl.one.err.t"),
body: languageService.translate("msg.dl.one.err.b", { error: message })
};
} catch {
return {
title: "Download failed",
body: message
};
}
};
const handleDownload = async (type) => {
const releaseLock = acquireDownloadLock();
if (!releaseLock) return;
const notifyError = (title, body) => {
userscript.notification({
title,
text: body
});
};
try {
const languageService = getLanguageService();
const mediaItems = gallerySignals.mediaItems;
const mediaService = getMediaService();
const downloadService = await getLazyDownloadService();
if (type === "current") {
const currentMedia = mediaItems[gallerySignals.currentIndex];
if (currentMedia) {
let blob;
try {
const pending = mediaService.getCachedMedia(currentMedia.url);
if (pending) blob = await pending;
} catch {}
const result = await downloadService.downloadSingle(currentMedia, { ...blob ? { blob } : {} });
if (!result.success) {
const error = result.error || "Unknown error";
const title = languageService.translate("msg.dl.one.err.t");
const body = languageService.translate("msg.dl.one.err.b", { error });
setError(body);
notifyError(title, body);
}
}
} else {
const prefetchedBlobs =  new Map();
for (const item of mediaItems) {
if (!item) continue;
const pending = mediaService.getCachedMedia(item.url);
if (!pending) continue;
prefetchedBlobs.set(item.url, pending);
}
const result = await downloadService.downloadBulk([...mediaItems], { ...prefetchedBlobs.size > 0 ? { prefetchedBlobs } : {} });
if (!result.success) {
if (result.filesSuccessful === 0) {
const title = languageService.translate("msg.dl.allFail.t");
const body = languageService.translate("msg.dl.allFail.b");
setError(body);
notifyError(title, body);
} else {
const error = result.error || "Failed to save ZIP file";
const title = languageService.translate("msg.dl.one.err.t");
const body = languageService.translate("msg.dl.one.err.b", { error });
setError(body);
notifyError(title, body);
}
return;
}
if (result.status === "partial") {
const failures = Math.max(0, result.filesProcessed - result.filesSuccessful);
if (failures > 0) {
const title = languageService.translate("msg.dl.part.t");
const body = languageService.translate("msg.dl.part.b", { count: failures });
setError(body);
notifyError(title, body);
}
}
}
} catch (error) {
logger.error("Download failed", error);
const { title, body } = getDownloadErrorNotification(error);
setError(body);
notifyError(title, body);
} finally {
releaseLock();
}
};
return { handleDownload };
}
async function getLazyDownloadService() {
return getDownloadOrchestrator();
}
var _tmpl$$1 =  template(`<div data-xeg-gallery-container>`);
var DISPOSE_SYMBOL = Symbol();
function mountGallery(container, element) {
const host = container;
host[DISPOSE_SYMBOL]?.();
host[DISPOSE_SYMBOL] = render(typeof element === "function" ? element : () => element ?? null, host);
return container;
}
function unmountGallery(container) {
const host = container;
host[DISPOSE_SYMBOL]?.();
delete host[DISPOSE_SYMBOL];
container.replaceChildren();
}
function GalleryContainer(props) {
const [local] = splitProps(props, [
"children",
"onClose",
"className"
]);
const classes = cx("xeg-gallery-overlay", "xeg-gallery-container", local.className);
onMount(() => {
const handler = (event) => {
const keyboardEvent = event;
if (keyboardEvent.key === "Escape") {
keyboardEvent.preventDefault();
keyboardEvent.stopPropagation();
local.onClose?.();
}
};
const eventManager = EventManager.getInstance();
const listenerId = eventManager.addEventListener(document, "keydown", handler);
onCleanup(() => {
if (listenerId) eventManager.removeListener(listenerId);
});
});
return (() => {
var _el$ = _tmpl$$1();
className(_el$, classes);
insert(_el$, () => local.children);
return _el$;
})();
}
var _tmpl$ =  template(`<div aria-live=polite data-xeg-error-boundary role=alert><p class=xeg-error-boundary__title></p><p class=xeg-error-boundary__body></p><button class=xeg-error-boundary__action type=button>Retry`);
function stringifyError(error) {
if (error instanceof Error && error.message) return error.message;
try {
return String(error);
} catch {
return "Unknown error";
}
}
function translateError(error) {
try {
const lang = getLanguageService();
return {
title: lang.translate("msg.err.t"),
body: lang.translate("msg.err.b", { error: stringifyError(error) })
};
} catch {
return {
body: stringifyError(error),
title: "Unexpected error"
};
}
}
function ErrorBoundary(props) {
let lastError;
const [caughtError, setCaughtError] = createSignal(void 0);
const [mounted, setMounted] = createSignal(true);
const notifyError = (error) => {
if (lastError === error) return;
lastError = error;
const { title, body } = translateError(error);
getUserscript().notification({
title,
text: body
});
};
const handleRetry = () => {
lastError = void 0;
setCaughtError(void 0);
setMounted(false);
queueMicrotask(() => setMounted(true));
};
return [createComponent(Show, {
get when() {
return mounted();
},
get children() {
return createComponent(ErrorBoundary$1, {
fallback: (error) => {
notifyError(error);
setCaughtError(error);
return null;
},
get children() {
return props.children;
}
});
}
}), createComponent(Show, {
get when() {
return caughtError();
},
children: (error) => {
const { title, body } = translateError(error());
return (() => {
var _el$ = _tmpl$(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling, _el$4 = _el$3.nextSibling;
insert(_el$2, title);
insert(_el$3, body);
_el$4.$$click = handleRetry;
return _el$;
})();
}
})];
}
delegateEvents(["click"]);
function GalleryRoot(props) {
const themeService = getThemeService();
const languageService = getLanguageService();
const [currentTheme, setCurrentTheme] = createSignal(themeService.getCurrentTheme());
const [currentLanguage, setCurrentLanguage] = createSignal(languageService.getCurrentLanguage());
const unbindTheme = themeService.onThemeChange((_, setting) => setCurrentTheme(setting));
const unbindLanguage = languageService.onLanguageChange((lang) => setCurrentLanguage(lang));
onCleanup(() => {
unbindTheme();
unbindLanguage();
});
return createComponent(GalleryContainer, {
get onClose() {
return props.onClose;
},
get className() {
return `${CSS.CLASSES.RENDERER} ${CSS.CLASSES.ROOT} xeg-theme-scope`;
},
get ["data-theme"]() {
return currentTheme();
},
get ["data-language"]() {
return currentLanguage();
},
get children() {
return createComponent(ErrorBoundary, { get children() {
return createComponent(VerticalGalleryView, {
get onClose() {
return props.onClose;
},
onPrevious: () => navigatePrevious("button"),
onNext: () => navigateNext("button"),
onDownloadCurrent: () => props.onDownloadCurrent(),
onDownloadAll: () => props.onDownloadAll(),
get className() {
return CSS.CLASSES.VERTICAL_VIEW;
}
});
} });
}
});
}
var GalleryRenderer = class {
container = null;
isMounting = false;
stateUnsubscribe = null;
onCloseCallback = null;
downloadHandler;
constructor() {
this.downloadHandler = useGalleryDownload().handleDownload;
this.setupStateSubscription();
}
setOnCloseCallback(onClose) {
this.onCloseCallback = onClose;
}
setupStateSubscription() {
this.stateUnsubscribe = effectSafe(() => {
if (gallerySignals.isOpen && !this.container) this.renderGallery();
else if (!gallerySignals.isOpen && this.container) this.cleanupGallery();
});
}
renderGallery() {
if (this.isMounting || this.container) return;
if (!gallerySignals.isOpen || gallerySignals.mediaItems.length === 0) return;
this.isMounting = true;
try {
this.createContainer();
this.renderComponent();
} catch (error) {
logger.error("Render failed", error);
this.cleanupContainer();
this.container = null;
setError(normalizeErrorMessage(error));
} finally {
this.isMounting = false;
}
}
createContainer() {
this.cleanupContainer();
this.container = document.createElement("div");
this.container.className = CSS.CLASSES.RENDERER;
this.container.setAttribute("data-renderer", "gallery");
document.body.appendChild(this.container);
}
renderComponent() {
if (!this.container) return;
const handleClose = () => {
closeGallery();
this.onCloseCallback?.();
};
mountGallery(this.container, () => {
const _self$ = this;
return createComponent(GalleryRoot, {
onClose: handleClose,
onDownloadCurrent: () => _self$.downloadHandler("current"),
onDownloadAll: () => _self$.downloadHandler("all")
});
});
}
cleanupGallery() {
this.isMounting = false;
this.cleanupContainer();
}
cleanupContainer() {
if (this.container) {
const container = this.container;
try {
unmountGallery(container);
} catch (error) {}
try {
container.remove();
} catch (error) {} finally {
this.container = null;
}
}
}
async render(mediaItems, renderOptions) {
openGallery(mediaItems, renderOptions?.startIndex ?? 0);
}
close() {
if (!gallerySignals.isOpen) return;
closeGallery();
this.onCloseCallback?.();
}
isRendering() {
return !!(this.container && gallerySignals.isOpen);
}
destroy() {
this.stateUnsubscribe?.();
this.stateUnsubscribe = null;
this.cleanupGallery();
}
};
var migrations = { "1.0.0": (input) => {
const next = { ...input };
next.gallery = {
...next.gallery,
enableKeyboardNav: true
};
return next;
} };
function pruneWithTemplate(input, template) {
if (!isRecord(input)) return {};
const out = {};
for (const key of Object.keys(template)) {
const tplVal = template[key];
const inVal = input[key];
if (inVal === void 0) continue;
if (isRecord(tplVal) && !Array.isArray(tplVal)) out[key] = pruneWithTemplate(inVal, tplVal);
else out[key] = inVal;
}
return out;
}
function fillWithDefaults(settings, nowMs) {
const pruned = pruneWithTemplate(settings, DEFAULT_SETTINGS);
const categories = {
gallery: DEFAULT_SETTINGS.gallery,
toolbar: DEFAULT_SETTINGS.toolbar,
download: DEFAULT_SETTINGS.download,
accessibility: DEFAULT_SETTINGS.accessibility,
features: DEFAULT_SETTINGS.features
};
const merged = {
...DEFAULT_SETTINGS,
...pruned
};
for (const [key, defaults] of Object.entries(categories)) merged[key] = {
...defaults,
...pruned[key] ?? {}
};
return {
...merged,
version: DEFAULT_SETTINGS.version,
lastModified: nowMs
};
}
function migrateSettings(input, nowMs) {
let working = { ...input };
const mig = migrations[input.version];
if (typeof mig === "function") try {
working = mig(working);
} catch {}
return fillWithDefaults(working, nowMs);
}
var SETTINGS_SCHEMA_HASH = "1";
function computeCurrentSettingsSchemaHash() {
return SETTINGS_SCHEMA_HASH;
}
var PersistentSettingsRepository = class {
storage = getPersistentStorage();
schemaHash = computeCurrentSettingsSchemaHash();
async load() {
try {
const stored = await this.storage.get(APP_SETTINGS_STORAGE_KEY);
if (!stored) {
const defaults = createDefaultSettings();
await this.persist(defaults).catch(() => {});
return globalThis.structuredClone(defaults);
}
const migrated = migrateSettings(stored, Date.now());
if (stored.__schemaHash !== this.schemaHash) await this.persist(migrated).catch(() => {});
return globalThis.structuredClone(migrated);
} catch (error) {
const defaults = createDefaultSettings();
await this.persist(defaults).catch(() => {});
return globalThis.structuredClone(defaults);
}
}
async save(settings) {
try {
await this.persist(settings);
} catch (error) {
logger.error("[SettingsRepository] Save operation failed", error);
throw error;
}
}
async persist(settings) {
await this.storage.set(APP_SETTINGS_STORAGE_KEY, {
...settings,
__schemaHash: this.schemaHash
});
}
};
var FORBIDDEN_PATH_KEYS = [
"__proto__",
"constructor",
"prototype"
];
function isSafePathKey(key) {
return key !== "" && !FORBIDDEN_PATH_KEYS.includes(key);
}
function resolveNestedPath(source, path) {
if (typeof path !== "string" || path === "") return;
let current = source;
const segments = path.split(".");
for (const segment of segments) {
if (!isSafePathKey(segment)) return;
if (current === null || typeof current !== "object") return;
current = current[segment];
}
return current;
}
function assignNestedPath(target, path, value) {
if (target === null || typeof target !== "object") return false;
if (typeof path !== "string" || path === "") return false;
const segments = path.split(".");
const last = segments[segments.length - 1];
if (!last || !isSafePathKey(last)) return false;
let current = target;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
if (!segment || !isSafePathKey(segment)) return false;
const existing = Object.hasOwn(current, segment) ? current[segment] : void 0;
if (existing === null || typeof existing !== "object") {
const next = Object.create(null);
current[segment] = next;
current = next;
continue;
}
current = existing;
}
current[last] = value;
return true;
}
function normalizeFeatureFlags(features) {
const featureKeys = Object.keys(DEFAULT_SETTINGS.features);
const featureDefaults = DEFAULT_SETTINGS.features;
return Object.freeze(featureKeys.reduce((acc, key) => {
const candidate = features?.[key];
acc[key] = typeof candidate === "boolean" ? candidate : featureDefaults[key];
return acc;
}, {}));
}
var _settingsInstance = null;
var SettingsService = class SettingsService {
_initialized = false;
static getInstance() {
if (!_settingsInstance) _settingsInstance = new SettingsService();
return _settingsInstance;
}
settings = createDefaultSettings();
featureMap = normalizeFeatureFlags(this.settings.features);
listeners =  new Set();
constructor(repository = new PersistentSettingsRepository()) {
this.repository = repository;
}
async initialize() {
if (this._initialized) return;
this.settings = await this.repository.load();
this.refreshFeatureMap();
this._initialized = true;
}
destroy() {
if (!this._initialized) return;
this.listeners.clear();
this._initialized = false;
}
isInitialized() {
return this._initialized;
}
getAllSettings() {
this.assertInitialized();
return globalThis.structuredClone(this.settings);
}
get(key) {
this.assertInitialized();
const value = resolveNestedPath(this.settings, key);
return value === void 0 ? this.getDefaultValue(key) : value;
}
async set(key, value) {
this.assertInitialized();
if (!this.isValid(key, value)) throw new Error(`Invalid setting value for ${key}`);
const oldValue = this.get(key);
if (!assignNestedPath(this.settings, key, value)) throw new Error(`Failed to assign setting value for ${key}`);
this.settings.lastModified = Date.now();
this.refreshFeatureMap();
this.notifyListeners({
key,
oldValue,
newValue: value,
timestamp: Date.now(),
status: "success"
});
await this.persist();
}
async updateBatch(updates) {
this.assertInitialized();
const entries = Object.entries(updates);
for (const [key, value] of entries) if (!this.isValid(key, value)) throw new Error(`Invalid setting value for ${key}`);
const previous = globalThis.structuredClone(this.settings);
const timestamp = Date.now();
for (const [key, value] of entries) if (!assignNestedPath(this.settings, key, value)) throw new Error(`Failed to assign setting value for ${key}`);
this.settings.lastModified = timestamp;
this.refreshFeatureMap();
for (const [key, value] of entries) {
const oldValue = resolveNestedPath(previous, key);
this.notifyListeners({
key,
oldValue,
newValue: value,
timestamp,
status: "success"
});
}
await this.persist();
}
async resetToDefaults(category) {
this.assertInitialized();
const previous = this.getAllSettings();
if (!category) this.settings = createDefaultSettings();
else if (category in DEFAULT_SETTINGS) {
const defaultValue = DEFAULT_SETTINGS[category];
if (defaultValue !== void 0) Object.assign(this.settings, { [category]: globalThis.structuredClone(defaultValue) });
}
this.settings.lastModified = Date.now();
this.refreshFeatureMap();
this.notifyListeners({
key: category ?? "all",
oldValue: previous,
newValue: this.getAllSettings(),
timestamp: Date.now(),
status: "success"
});
await this.persist();
}
subscribe(listener) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
exportSettings() {
this.assertInitialized();
return JSON.stringify(this.settings, null, 2);
}
async importSettings(jsonString) {
this.assertInitialized();
try {
const imported = JSON.parse(jsonString);
if (!imported || typeof imported !== "object") throw new Error("Invalid settings");
const previous = this.getAllSettings();
const nowMs = Date.now();
this.settings = migrateSettings(imported, nowMs);
this.settings.lastModified = nowMs;
this.refreshFeatureMap();
this.notifyListeners({
key: "all",
oldValue: previous,
newValue: this.getAllSettings(),
timestamp: nowMs,
status: "success"
});
await this.persist();
} catch (error) {
throw error;
}
}
getFeatureMap() {
this.assertInitialized();
return Object.freeze({ ...this.featureMap });
}
refreshFeatureMap() {
this.featureMap = normalizeFeatureFlags(this.settings.features);
}
async persist() {
await this.repository.save(this.settings);
}
isValid(key, value) {
const def = this.getDefaultValue(key);
if (def === void 0) return true;
const type = Array.isArray(def) ? "array" : typeof def;
if (type === "array") return Array.isArray(value);
if (type === "object") return typeof value === "object" && value !== null;
return typeof value === type;
}
getDefaultValue(key) {
return resolveNestedPath(DEFAULT_SETTINGS, key);
}
notifyListeners(event) {
this.listeners.forEach((listener) => {
try {
listener(event);
} catch (error) {}
});
}
assertInitialized() {
if (!this.isInitialized()) throw new Error("SettingsService must be initialized before use");
}
};
function ensureRendererRegistered() {
if (hasRenderer()) return;
registerRenderer(new GalleryRenderer());
}
async function initializeSettingsService() {
const existingSettings = tryGetSettingsManager();
if (existingSettings) {
if (existingSettings.isInitialized?.() === false && existingSettings.initialize) await existingSettings.initialize();
return;
}
const service = new SettingsService();
await service.initialize();
registerSettingsManager(service);
}
async function initializeGalleryServices() {
try {
await initializeSettingsService();
} catch (error) {
settingsErrorReporter.warn(error, { code: "SETTINGS_SERVICE_INIT_FAILED" });
getUserscript().notification({
title: "Settings unavailable",
text: "Defaults will be used until settings load."
});
}
}
async function initializeGalleryApp() {
try {
ensureRendererRegistered();
const galleryApp = new GalleryApp();
await galleryApp.initialize();
return galleryApp;
} catch (error) {
galleryErrorReporter.error(error, { code: "GALLERY_APP_INIT_FAILED" });
throw error;
}
}
async function executeStage(stage) {
const startTime = performance.now();
if (stage.shouldRun && !stage.shouldRun()) return {
label: stage.label,
success: true,
skipped: true,
optional: stage.optional ?? false,
error: void 0,
durationMs: 0
};
try {
await Promise.resolve(stage.run());
const durationMs = performance.now() - startTime;
return {
label: stage.label,
success: true,
skipped: false,
optional: stage.optional ?? false,
error: void 0,
durationMs
};
} catch (error) {
const durationMs = performance.now() - startTime;
if (stage.optional) bootstrapErrorReporter.warn(error, {
code: "STAGE_OPTIONAL_FAILED",
metadata: {
stage: stage.label,
durationMs
}
});
else bootstrapErrorReporter.error(error, {
code: "STAGE_FAILED",
metadata: {
stage: stage.label,
durationMs
}
});
return {
label: stage.label,
success: false,
skipped: false,
optional: stage.optional ?? false,
error,
durationMs
};
}
}
async function executeStages(stages, options) {
const results = [];
const stopOnFailure = options?.stopOnFailure ?? true;
for (const stage of stages) {
const result = await executeStage(stage);
results.push(result);
if (!result.success && !result.optional && stopOnFailure) {
logger.error(`[bootstrap] ❌ Critical stage failed: ${stage.label}`);
break;
}
}
return results;
}
var formatErrorLocation = (filename, lineno, colno) => filename && `${filename}:${lineno ?? 0}:${colno ?? 0}`;
var formatRejectionMessage = (reason) => {
if (reason instanceof Error) return reason.message;
if (typeof reason === "string") return reason;
return `Unhandled rejection: ${String(reason)}`;
};
var _errorHandlerInstance = null;
var GlobalErrorHandler = class GlobalErrorHandler {
isInitialized = false;
controller = null;
constructor() {}
static getInstance() {
if (!_errorHandlerInstance) _errorHandlerInstance = new GlobalErrorHandler();
return _errorHandlerInstance;
}
errorListener = (event) => {
formatErrorLocation(event.filename, event.lineno, event.colno);
};
rejectionListener = (event) => {
formatRejectionMessage(event.reason);
};
initialize() {
if (this.isInitialized || typeof window === "undefined") return;
const eventManager = EventManager.getInstance();
this.controller = new AbortController();
const onError = (evt) => {
this.errorListener(evt);
};
const onUnhandledRejection = (evt) => {
this.rejectionListener(evt);
};
eventManager.addEventListener(window, "error", onError, {
signal: this.controller.signal,
context: "global-error-handler"
});
eventManager.addEventListener(window, "unhandledrejection", onUnhandledRejection, {
signal: this.controller.signal,
context: "global-error-handler"
});
this.isInitialized = true;
}
destroy() {
if (!this.isInitialized || typeof window === "undefined") return;
this.controller?.abort();
this.controller = null;
this.isInitialized = false;
}
};
var lifecycleState = {
started: false,
startPromise: null,
galleryApp: null
};
function wireGlobalEvents(onBeforeUnload) {
if (!(typeof window !== "undefined" && !!window.addEventListener)) return () => {};
let disposed = false;
const eventManager = EventManager.getInstance();
const controller = new AbortController();
const invokeOnce = () => {
if (disposed) return;
disposed = true;
controller.abort();
onBeforeUnload();
};
eventManager.addEventListener(window, "pagehide", invokeOnce, {
once: true,
passive: true,
signal: controller.signal,
context: "bootstrap:pagehide"
});
return () => {
if (disposed) return;
disposed = true;
controller.abort();
};
}
var globalEventTeardown = null;
function tearDownGlobalEventHandlers() {
if (!globalEventTeardown) return;
const teardown = globalEventTeardown;
globalEventTeardown = null;
try {
teardown();
} catch (error) {}
}
function setupGlobalEventHandlers() {
tearDownGlobalEventHandlers();
globalEventTeardown = wireGlobalEvents(() => {
cleanup().catch((error) => {});
});
}
async function runOptionalCleanup(label, task) {
try {
await task();
} catch (error) {}
}
function buildStages() {
return [
{
label: "Error handler",
run: () => {
GlobalErrorHandler.getInstance().initialize();
}
},
{
label: "Gallery services",
run: async () => {
try {
await initializeGalleryServices();
} catch (error) {
bootstrapErrorReporter.warn(error, { code: "GALLERY_SERVICES_INIT_FAILED" });
throw error;
}
},
optional: true
},
{
label: "Base services",
run: async () => {
try {
await initializeCoreBaseServices();
} catch (error) {
bootstrapErrorReporter.warn(error, { code: "BASE_SERVICES_INIT_FAILED" });
throw error;
}
},
optional: true
},
{
label: "Global event wiring",
run: setupGlobalEventHandlers
},
{
label: "Gallery initialization",
run: initializeGallery,
shouldRun: () => true
}
];
}
async function runBootstrapStages() {
const failed = (await executeStages(buildStages(), { stopOnFailure: true })).find((r) => !r.success && !r.optional);
if (failed) throw failed.error ??  new Error(`Bootstrap stage failed: ${failed.label}`);
}
async function initializeGallery() {
try {
lifecycleState.galleryApp = await initializeGalleryApp();
} catch (error) {
lifecycleState.galleryApp = null;
galleryErrorReporter.error(error, { code: "GALLERY_INIT_FAILED" });
throw error;
}
}
async function cleanup() {
try {
await runOptionalCleanup("gallery", async () => {
const app = lifecycleState.galleryApp;
lifecycleState.galleryApp = null;
if (app) await app.cleanup();
});
tearDownGlobalEventHandlers();
await runOptionalCleanup("timers", () => globalTimerManager.cleanup());
await runOptionalCleanup("error-handler", () => GlobalErrorHandler.getInstance().destroy());
lifecycleState.started = false;
} catch (error) {
bootstrapErrorReporter.error(error, { code: "CLEANUP_FAILED" });
throw error;
}
}
async function startApplication() {
if (lifecycleState.startPromise) return lifecycleState.startPromise;
if (lifecycleState.started) return;
lifecycleState.startPromise = (async () => {
await runBootstrapStages();
lifecycleState.started = true;
})().catch((error) => {
lifecycleState.started = false;
bootstrapErrorReporter.error(error, { code: "APP_INIT_FAILED" });
throw error;
}).finally(() => {
lifecycleState.startPromise = null;
});
return lifecycleState.startPromise;
}
startApplication().catch(() => {});
})();