1// Generated from src/app/lightbox.js. Do not edit directly. 2
3// src/lib/site-icon-style.js 4var SITE_ICON_ATTRIBUTES = Object.freeze({ 5 "stroke-width": 1.5, 6 "stroke-linecap": "round", 7 "stroke-linejoin": "miter" 8}); 9var SITE_CHEVRON_LEFT_SVG = '<svg class="ui-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="miter" aria-hidden="true" focusable="false"><path d="m15 6-6 6 6 6"/></svg>'; 10var SITE_CHEVRON_RIGHT_SVG = '<svg class="ui-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="miter" aria-hidden="true" focusable="false"><path d="m9 6 6 6-6 6"/></svg>'; 11
12// src/lib/tag-vocabulary.js 13var TAG_DIMENSIONS = Object.freeze([ 14 { id: "medium", label: "åªä»ææ³", tags: ["æå½±", "æç»", "线æ", "æç»", "书æ³", "水墨", "水彩", "çç»", "åªçº¸", "æ¼è´´", "æ¶é¸¦", "贴纸", "ç¹éµ", "åè°ç½ç¹", "å ä½", "ç«ä½", "æµ®é", "åªå½±", "å¾å½¢å"] }, 15 { id: "layout", label: "æå¾çå¼", tags: ["çç½", "大å", "æ¡æ¯", "å± ä¸", "æç®", "ç½æ ¼", "æµ æ¯æ·±", "éè½", "ç¹å", "åæ ¼", "å¾®è·", "éµå", "æ¾å°æå¾", "俯è§", "ä»°è§", "å±å "] }, 16 { id: "type", label: "åä½", tags: ["æå", "衬线", "æ 衬线", "æä½"] }, 17 { id: "light", label: "å 线质æ", tags: ["é«æåº¦", "æåº", "é¢ç²", "èç", "æ¸å", "纸æ", "åå ", "æå ", "ç»ç", "å æ", "éå±", "å æ", "ç»ç©", "ä¾§å ", "éå ", "龿"] }, 18 { id: "palette", label: "è²è°", tags: ["æ¸ é", "é«é¥±å", "å·è°", "æè°", "é»ç½", "æµ ç°", "ä¸å½çº¢"] }, 19 { id: "mood", label: "æ 绪æ°è´¨", tags: ["䏿¹", "éè°§", "æå§å", "é è´", "轻快", "ä»ªå¼æ", "ç«¥ç", "åºé", "ä¿ç®", "å·é", "æ²»æ", "å å¶", "å¤å¤", "åè¿«æ", "ç¥ç§", "卿", "å·å³»", "ç²ç²"] }, 20 { id: "subject", label: "颿å 容", tags: ["人å", "è·è±", "æ¤ç©", "è±å", "éªæ¯", "丰æ¶", "山水", "äºæµ", "åå¸", "æ·å¤", "éç©", "åå¡", "礼ç", "å°å¾", "ä¸å»", "æ°ä¿", "æå"] }, 21 { id: "use", label: "åºæ¯ç¨é", tags: ["ç¼è¾æ", "åå", "è令", "ç§ææ", "æ¡£æ¡æ", "çµå½±æ", "ä¿é", "èåº", "èå°æ", "æ©å®", "éå", "ç§æ®å¾è§£", "纪å®", "è¡å¤´", "çæ´»æ", "éå¤", "åå¡", "æè", "å¡ç", "å计æ¶"] } 22]); 23var TAG_SYNONYMS = Object.freeze({ 24 "å·¨å": "大å", 25 "å·¨åç©¿æ": "大å", 26 "童趣": "ç«¥ç", 27 "æ¸ æ°": "æ¸ é", 28 "æ¸ ç½": "æ¸ é", 29 "æ´å": "æ¸ é", 30 "æ¸ äº®": "æ¸ é", 31 "æäº®": "é«æåº¦", 32 "æ¸ é ": "é è´", 33 "æå¿«": "轻快", 34 "æ¸ ç": "轻快", 35 "æ¾å¼": "轻快", 36 "è½»ç": "轻快", 37 "å æ°": "轻快", 38 "çç¾": "ä¿ç®", 39 "温æ ": "æ²»æ", 40 "浪漫": "æ²»æ", 41 "温润": "æ²»æ", 42 "æ¬ç": "ç¥ç§", 43 "梦幻": "ç¥ç§", 44 "å¨å¿": "卿", 45 "é度æ": "卿", 46 "çæ§": "å·é", 47 "ææ§": "å¤å¤", 48 "ç§æ®æµ·æ¥": "ç§æ®å¾è§£", 49 "ç§æ®ä¿¡æ¯å¾": "ç§æ®å¾è§£", 50 "ä¿¡æ¯å¾": "ç§æ®å¾è§£", 51 "å¾è¡¨": "ç§æ®å¾è§£", 52 "å¾é´": "ç§æ®å¾è§£", 53 "çºªå®æå½±": "纪å®", 54 "å±éæ": "éå", 55 "æä½": "æç»", 56 "ä¹å®«æ ¼": "åæ ¼", 57 "æå°": "çç»", 58 "é£ç½": "书æ³", 59 "å®ä½": "æä½", 60 "æè¾¹": "线æ", 61 "è²å": "å¾å½¢å", 62 "å ä½å¾å½¢": "å ä½", 63 "åè§": "å ä½", 64 "æå": "éè½", 65 "æ¾å°": "æ¾å°æå¾", 66 "ç¯å½¢": "æ¾å°æå¾", 67 "é¿å½±": "ä¾§å ", 68 "é²ç æ°´ç ": "èç", 69 "æ°´é¢è´¨æ": "èç", 70 "å°ç« ": "䏿¹", 71 "è±çª": "䏿¹", 72 "å·è½´": "䏿¹", 73 "è·å ç´ ": "è·è±", 74 "ééª": "éªæ¯", 75 "稻浪": "丰æ¶", 76 "麦ç©": "丰æ¶", 77 "èªç¶": "æ¤ç©", 78 "ç°é¶": "é»ç½", 79 "æè²": "é«é¥±å", 80 "è§å ": "é«é¥±å", 81 "é¶ç°": "æµ ç°", 82 "æ·±ç°": "æµ ç°", 83 "é¾è": "å·è°", 84 "ç²è": "å·è°", 85 "天空è": "å·è°", 86 "èè·ç»¿": "å·è°", 87 "é绿": "å·è°", 88 "è绿": "å·è°", 89 "éè": "å·è°", 90 "æµ ç²": "æè°", 91 "ææ©": "æè°", 92 "æ æª¬é»": "æè°", 93 "å塿£": "æè°", 94 "æè²": "æè°", 95 "éé": "æè°", 96 "æµ é": "æè°", 97 "æ¥å é»": "æè°", 98 "麦é»": "æè°", 99 "é»ç»¿": "æè°", 100 "ç°é»": "æè°", 101 "ç±³ç½": "é«æåº¦", 102 "奶ç½": "é«æåº¦", 103 "纸ç½": "é«æåº¦", 104 "æ¥æ": "è令", 105 "夿": "è令", 106 "嬿": "è令", 107 "鍿": "è令", 108 "æ°ç»¿": "è令", 109 "é红": "è令", 110 "æ¨é¾": "è令", 111 "è鸣": "è令", 112 "æ¡å½±": "è令", 113 "watercolor": "水彩", 114 "水彩é£": "水彩" 115}); 116function normalizeTagKey(tag) { 117 return String(tag ?? "").trim().toLowerCase().replace(/\s+/g, ""); 118} 119function buildTagIndex(dimensions = TAG_DIMENSIONS) { 120 const dimensionOrder = []; 121 const tagToDimension = /* @__PURE__ */ new Map(); 122 const tagOrder = /* @__PURE__ */ new Map(); 123 const labelByTag = /* @__PURE__ */ new Map(); 124 for (const dim of Array.isArray(dimensions) ? dimensions : []) {
125 const id = String(dim?.id ?? ""); 126 if (!id) continue; 127 const tags = (Array.isArray(dim?.tags) ? dim.tags : []).map(String).filter(Boolean); 128 dimensionOrder.push({ id, label: String(dim?.label ?? id) }); 129 tagOrder.set(id, tags.map(normalizeTagKey)); 130 for (const tag of tags) { 131 const key = normalizeTagKey(tag); 132 if (!tagToDimension.has(key)) { 133 tagToDimension.set(key, id); 134 labelByTag.set(key, tag); 135 } 136 } 137 } 138 return { dimensionOrder, tagToDimension, tagOrder, labelByTag }; 139} 140
141// src/app/detail-tags.js 142function buildDetailTagEntries(tags, index = buildTagIndex()) { 143 const byKey = /* @__PURE__ */ new Map(); 144 for (const raw of Array.isArray(tags) ? tags : []) { 145 const key = normalizeTagKey(raw); 146 if (!key || byKey.has(key)) continue; 147 byKey.set(key, String(raw)); 148 } 149 const entries = []; 150 for (const { id, label } of index.dimensionOrder) { 151 for (const key of index.tagOrder.get(id) || []) { 152 if (!byKey.has(key)) continue; 153 entries.push({ tag: index.labelByTag.get(key) ?? byKey.get(key), dimensionId: id, dimensionLabel: label }); 154 byKey.delete(key); 155 } 156 } 157 for (const tag of byKey.values()) entries.push({ tag, dimensionId: "", dimensionLabel: "" }); 158 return entries; 159} 160
161// src/app/app-cdn-routing.js 162var IMAGE_CDN_BASE = "https://img.xiaoxiaodong.ai"; 163var OBJECT_PATH_PREFIX = "/api/object/"; 164var OBJECT_THUMB_SEGMENT = "data/thumbs/"; 165var OBJECT_FULL_SEGMENT = "data/full/"; 166var MEMBER_ORIGIN = "https://vip.xiaoxiaodong.ai"; 167var PRIVATE_MEMBER_OBJECT_PREFIXES = [ 168 `${OBJECT_PATH_PREFIX}${OBJECT_THUMB_SEGMENT}generated-history/`, 169 `${OBJECT_PATH_PREFIX}${OBJECT_FULL_SEGMENT}generated-history/`, 170 `${OBJECT_PATH_PREFIX}${OBJECT_THUMB_SEGMENT}media-library/`, 171 `${OBJECT_PATH_PREFIX}${OBJECT_FULL_SEGMENT}media-library/` 172]; 173function encodedObjectPath(value = "") { 174 let decoded = ""; 175 try { 176 decoded = decodeURIComponent(String(value || "")); 177 } catch { 178 return ""; 179 } 180 const key = decoded.replace(/^\/+/, ""); 181 if (!key.startsWith(OBJECT_THUMB_SEGMENT) && !key.startsWith(OBJECT_FULL_SEGMENT)) return ""; 182 return `${OBJECT_PATH_PREFIX}${key.split("/").map(encodeURIComponent).join("/")}`; 183} 184function sameOriginObjectUrl(value, baseHref = globalThis.location?.href || MEMBER_ORIGIN) { 185 const raw = String(value || "").trim(); 186 if (!raw || raw.startsWith("data:") || raw.startsWith("blob:")) return raw; 187 if (raw.startsWith(OBJECT_PATH_PREFIX)) return raw; 188 const directObjectPath = encodedObjectPath(raw); 189 if (directObjectPath) return directObjectPath; 190 try { 191 const base = new URL(baseHref || MEMBER_ORIGIN, MEMBER_ORIGIN); 192 const url = new URL(raw, base); 193 const isAbsolute = /^[a-z][a-z\d+.-]*:/i.test(raw) || raw.startsWith("//"); 194 const trustedOrigins = /* @__PURE__ */ new Set([base.origin, MEMBER_ORIGIN, new URL(IMAGE_CDN_BASE).origin]); 195 if (isAbsolute && !trustedOrigins.has(url.origin)) return raw; 196 if (url.pathname.startsWith(OBJECT_PATH_PREFIX)) return `${url.pathname}${url.search}`; 197 const objectPath = encodedObjectPath(url.pathname); 198 return objectPath ? `${objectPath}${url.search}` : raw; 199 } catch { 200 return raw; 201 } 202} 203function cdnMediaUrl(value) { 204 const path = sameOriginObjectUrl(value); 205 if (!path.startsWith(OBJECT_PATH_PREFIX)) return value; 206 let decoded; 207 try { 208 decoded = decodeURIComponent(path.split("?")[0]); 209 } catch { 210 return value; 211 } 212 if (PRIVATE_MEMBER_OBJECT_PREFIXES.some((prefix) => decoded.startsWith(prefix))) return path; 213 const url = new URL(path, MEMBER_ORIGIN); 214 url.searchParams.set(decoded.startsWith(`${OBJECT_PATH_PREFIX}${OBJECT_THUMB_SEGMENT}`) ? "public_preview" : "member_image", "1"); 215 return `${url.pathname}${url.search}`; 216} 217
218// src/lib/cover-version.js 219var COVER_KINDS = Object.freeze(["article", "skill"]); 220var COVER_LOCALES = Object.freeze(["zh-CN", "en"]); 221
222// src/app/responsive-image.js 223var DISPLAY_IMAGE_WIDTHS = Object.freeze([360, 540, 720, 1080, 1440]); 224var SLOW_NETWORK_MAX_WIDTH = 720; 225var LEGACY_KEY_RE = /^data\/(?:thumbs|full)\/(?:generated|generated-history|media-library|ingest|_display|en\/bundle)\//; 226function objectKeyAndSignature(path) { 227 const raw = String(path || "").trim(); 228 if (!raw || raw.startsWith("data:") || raw.startsWith("blob:")) return null; 229 let url; 230 try { 231 url = new URL(raw, "https://vip.xiaoxiaodong.ai"); 232 } catch { 233 return null; 234 } 235 if (!url.pathname.startsWith("/api/object/")) return null; 236 let key; 237 try { 238 key = decodeURIComponent(url.pathname.slice("/api/object/".length)); 239 } catch { 240 return null; 241 } 242 if (!/^data\/(?:thumbs|full)\/./.test(key) || LEGACY_KEY_RE.test(key)) return null; 243 return { key: key.replace(/^data\/full\//, "data/thumbs/"), pub: url.searchParams.get("pub") || "" }; 244} 245function naturalWidthFromRatio(ratio) { 246 const match = String(ratio || "").match(/^\s*(\d+(?:\.\d+)?)\s*[/:]\s*(\d+(?:\.\d+)?)\s*$/); 247 const width = Number(match?.[1]); 248 return width >= 200 ? Math.round(width) : 0; 249} 250function slowConnection(navigator = globalThis.navigator) { 251 const connection = navigator?.connection || navigator?.mozConnection || navigator?.webkitConnection; 252 if (connection?.saveData) return true; 253 return ["slow-2g", "2g", "3g"].includes(String(connection?.effectiveType || "").toLowerCase()); 254} 255function displayImageSources(path, { ratio = "", memberGrant = false, slow = slowConnection() } = {}) { 256 const source = objectKeyAndSignature(path); 257 if (!source || !source.pub && !memberGrant) return null; 258 const encoded = source.key.split("/").map(encodeURIComponent).join("/"); 259 const query = source.pub ? `?pub=${encodeURIComponent(source.pub)}` : ""; 260 const natural = naturalWidthFromRatio(ratio); 261 const candidates = []; 262 for (const width of DISPLAY_IMAGE_WIDTHS) { 263 if (slow && width > SLOW_NETWORK_MAX_WIDTH) break; 264 const descriptor = natural && width >= natural ? natural : width; 265 candidates.push({ url: `/api/image/w${width}/${encoded}${query}`, descriptor }); 266 if (natural && width >= natural) break; 267 } 268 if (!candidates.length) return null; 269 const fallback = candidates.filter((candidate) => candidate.descriptor <= 720).pop() || candidates[0]; 270 return { 271 src: fallback.url, 272 srcset: candidates.map((candidate) => `${candidate.url} ${candidate.descriptor}w`).join(", ") 273 }; 274} 275
276// src/app/lightbox.js 277var LIGHTBOX_WHEEL_ZOOM_INTENSITY = 22e-4; 278var LIGHTBOX_MIN_SCALE = 0.25; 279var LIGHTBOX_MAX_SCALE = 6; 280var LIGHTBOX_THUMB_WINDOW = 25; 281var LIGHTBOX_THUMB_THRESHOLD = 40; 282var LIGHTBOX_THUMB_HEIGHT = 44; 283var LIGHTBOX_THUMB_GAP = 8; 284function createLightboxModule({ document, state, els, routePathForTopic, onTopicSelect } = {}) { 285 const lb = { index: 0, scale: 1, x: 0, y: 0, dragging: false }; 286 let transientGallery = null; 287 let transientPreviousGallery = null; 288 let returnFocusTarget = null; 289 let dragged = false; 290 let renderedThumbItems = null; 291 let thumbWidths = []; 292 let thumbOffsets = [0]; 293 let activeThumb = null; 294 let thumbWindowStart = 0; 295 let thumbWindowEnd = 0; 296 let thumbScrollFrame = null; 297 let renderedTopicsKey = null; 298 let renderedTagsKey = null; 299 let topicCount = 0; 300 let trayMode = "closed"; 301 let metaOpen = false; 302 let metaPeek = false; 303 let compactTray = false; 304 let detailHome = null; 305 let detailNext = null; 306 let detailScrollTop = 0; 307 let detailCloseWasHidden = false; 308 let detailExpanded = false; 309 let controlTimer = null; 310 let fittedSize = null; 311 let imageGeneration = 0; 312 let imageReady = false; 313 let previewReady = false; 314 let imageFailed = false; 315 let displayState = null; 316 let placeholderRatio = 1; 317 let transformFrame = null; 318 let stageObserver = null; 319 let frameGeometry = null; 320 let appliedX = 0; 321 let appliedY = 0; 322 let appliedScale = 1; 323 const pointers = /* @__PURE__ */ new Map(); 324 let gesture = null; 325 let hadPinch = false; 326 let lastImagePointerType = "mouse"; 327 const win = document.defaultView; 328 const coarsePointer = win?.matchMedia?.("(pointer: coarse)"); 329 const english = () => document.documentElement.lang.startsWith("en"); 330 const ui = (hans, hant, en) => english() ? en : document.documentElement.lang === "zh-Hant" ? hant : hans; 331 function isCompactTray() { 332 return win.innerWidth <= 800 || win.innerHeight <= 620 || win.innerWidth > win.innerHeight && win.innerHeight < 700; 333 } 334 function defaultTrayMode() { 335 return (state.galleryItems || []).length > 1 && !compactTray ? "thumbs" : "closed"; 336 } 337 function setDetailExpanded(expanded) { 338 if (!detailHome) return; 339 detailExpanded = Boolean(expanded); 340 els.lightbox.classList.toggle("is-detail-expanded", detailExpanded); 341 els.lightboxDetailSlot.hidden = !detailExpanded; 342 els.lightboxDetailSlot.inert = !detailExpanded; 343 if (els.lightboxPanelToggle) { 344 els.lightboxPanelToggle.hidden = false; 345 const label = detailExpanded ? ui("æ¶èµ·å³ä¾§è¯¦æ ", "æ¶èµ·å³å´è©³æ ", "Collapse details panel") : ui("å±å¼å³ä¾§è¯¦æ ", "å±éå³å´è©³æ ", "Expand details panel"); 346 els.lightboxPanelToggle.innerHTML = detailExpanded ? SITE_CHEVRON_RIGHT_SVG : SITE_CHEVRON_LEFT_SVG; 347 els.lightboxPanelToggle.setAttribute("aria-label", label); 348 els.lightboxPanelToggle.setAttribute("title", label); 349 els.lightboxPanelToggle.setAttribute("aria-expanded", String(detailExpanded)); 350 } 351 metaOpen = false; 352 metaPeek = false; 353 syncTray(); 354 frameGeometry = null; 355 resizePlaceholder(); 356 lbApply(); 357 } 358 function borrowDetailPanel() { 359 if (detailHome) { 360 setDetailExpanded(!compactTray); 361 return true; 362 } 363 const panel = els.detailPanel; 364 if (!panel || panel.hidden || !document.body.classList.contains("is-detail-open") || panel.classList.contains("style-share-embedded-panel")) return false; 365 detailHome = panel.parentNode; 366 detailNext = panel.nextSibling; 367 detailScrollTop = els.detailBody?.scrollTop || 0; 368 const close = document.querySelector("#closeDetailButton"); 369 detailCloseWasHidden = Boolean(close?.hidden); 370 if (close) close.hidden = true; 371 els.lightboxDetailSlot.append(panel); 372 els.lightboxDetailSlot.inert = false; 373 setDetailExpanded(!compactTray); 374 return true; 375 } 376 function returnDetailPanel() { 377 if (!detailHome) return; 378 detailHome.insertBefore(els.detailPanel, detailNext); 379 const close = document.querySelector("#closeDetailButton"); 380 if (close) close.hidden = detailCloseWasHidden; 381 if (els.detailBody) els.detailBody.scrollTop = detailScrollTop; 382 els.lightboxDetailSlot.hidden = true; 383 els.lightboxDetailSlot.inert = true; 384 els.lightbox.classList.remove("is-detail-expanded"); 385 detailHome = null; 386 detailNext = null; 387 detailExpanded = false;
388 } 389 function syncTray() { 390 const multi = (state.galleryItems || []).length > 1; 391 if (!multi && trayMode === "thumbs") trayMode = "closed"; 392 const hasTags = Boolean(els.lightboxTags?.children.length); 393 const hasMeta = hasTags || topicCount > 0; 394 const canvasAvailable = !compactTray || !detailExpanded; 395 els.lightbox.classList.toggle("is-compact-tray", compactTray); 396 if (els.lightboxThumbs) els.lightboxThumbs.setAttribute("aria-orientation", compactTray ? "horizontal" : "vertical"); 397 if (els.lightboxTray) els.lightboxTray.hidden = !canvasAvailable || !(trayMode === "thumbs" || (metaOpen || metaPeek) && hasMeta); 398 if (els.lightboxThumbs) els.lightboxThumbs.hidden = !multi || trayMode !== "thumbs"; 399 if (els.lightboxMeta) els.lightboxMeta.hidden = !hasMeta || !metaOpen && !metaPeek; 400 if (els.lightboxTagsHeading) els.lightboxTagsHeading.hidden = !hasTags; 401 if (els.lightboxTopicsHeading) els.lightboxTopicsHeading.hidden = !topicCount; 402 if (els.lightboxTopics) els.lightboxTopics.hidden = !topicCount; 403 if (els.lightboxThumbToggle) { 404 els.lightboxThumbToggle.hidden = !canvasAvailable || !multi; 405 els.lightboxThumbToggle.setAttribute("aria-expanded", String(trayMode === "thumbs")); 406 els.lightboxThumbToggle.setAttribute("aria-label", trayMode === "thumbs" ? ui("æ¶èµ·ç¼©ç¥å¾", "æ¶èµ·ç¸®ç¥å", "Hide thumbnails") : ui("æ¾ç¤ºç¼©ç¥å¾", "顯示縮ç¥å", "Show thumbnails")); 407 } 408 if (els.lightboxTopicsToggle) { 409 els.lightboxTopicsToggle.hidden = !canvasAvailable || !hasMeta; 410 els.lightboxTopicsToggle.textContent = win.innerWidth <= 600 ? ui("ä¿¡æ¯", "è³è¨", "Info") : ui("å¾çä¿¡æ¯", "åçè³è¨", "Image info"); 411 els.lightboxTopicsToggle.setAttribute("aria-expanded", String(metaOpen)); 412 } 413 if (els.lightboxPanelCount) { 414 els.lightboxPanelCount.hidden = true; 415 els.lightboxPanelCount.textContent = multi ? `${lb.index + 1} / ${(state.galleryItems || []).length}` : ""; 416 } 417 if (els.lightboxLocate) els.lightboxLocate.hidden = true; 418 if (els.lightboxFit) els.lightboxFit.textContent = win.innerWidth <= 600 ? ui("éå", "é©å", "Fit") : ui("éåå±å¹", "é©åè¢å¹", "Fit to screen"); 419 } 420 function renderLightboxTags(item) { 421 const row = els.lightboxTags; 422 if (!row) return; 423 const tags = buildDetailTagEntries(item?.tags).map((entry) => entry.tag); 424 const key = JSON.stringify(tags); 425 row.hidden = tags.length === 0; 426 if (key === renderedTagsKey) { 427 syncTray(); 428 return; 429 } 430 renderedTagsKey = key; 431 row.setAttribute("aria-label", ui(`å½åæ ·å¼ æ ç¾ï¼${tags.join("ã")}`, `ç¶å樣張æ¨ç±¤ï¼${tags.join("ã")}`, `Current image tags: ${tags.join(", ")}`)); 432 row.replaceChildren(...tags.map((tag) => { 433 const chip = document.createElement("span"); 434 chip.className = "lightbox-tag"; 435 chip.setAttribute("role", "listitem"); 436 chip.textContent = tag; 437 return chip; 438 })); 439 syncTray(); 440 } 441 function renderLightboxTopics(item) { 442 const row = els.lightboxTopics; 443 if (!row) return; 444 const topics = []; 445 const seen = /* @__PURE__ */ new Set(); 446 for (const topic of Array.isArray(item?.sampleTopics) ? item.sampleTopics : []) { 447 const id = String(topic?.id || "").trim(); 448 const name = String(topic?.name || "").trim(); 449 if (!id || !name || seen.has(id)) continue; 450 seen.add(id); 451 if (typeof routePathForTopic === "function") topics.push({ id, name, href: routePathForTopic(id) }); 452 } 453 const key = JSON.stringify([english(), topics]); 454 topicCount = topics.length; 455 if (key === renderedTopicsKey) { 456 syncTray(); 457 return; 458 } 459 renderedTopicsKey = key; 460 row.setAttribute("aria-label", ui("å¾çæå±æ ç®", "åçæå±¬æ¬ç®", "Image categories")); 461 const focusedId = row.contains(document.activeElement) ? document.activeElement.dataset.topic : null; 462 row.replaceChildren(...topics.map(({ id, name, href }) => { 463 const link = document.createElement("a"); 464 link.href = href; 465 link.dataset.topic = id; 466 link.textContent = name; 467 link.setAttribute("aria-label", ui(`æ¥ç${name}æ ç®`, `æ¥ç${name}æ¬ç®`, `Browse ${name}`)); 468 link.addEventListener("click", (event) => { 469 if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 470 if (typeof onTopicSelect !== "function") return; 471 event.preventDefault(); 472 closeLightbox({ restoreFocus: false }); 473 onTopicSelect(id); 474 }); 475 return link; 476 })); 477 if (focusedId) { 478 const replacement = [...row.children].find((link) => link.dataset.topic === focusedId); 479 (replacement || (topics.length ? els.lightboxTopicsToggle : els.lightboxClose))?.focu
479s({ preventScroll: true }); 480 } 481 syncTray(); 482 } 483 function cancelTransformFrame() { 484 if (transformFrame !== null) win.cancelAnimationFrame(transformFrame); 485 transformFrame = null; 486 frameGeometry = null; 487 } 488 function geometry() { 489 if (!frameGeometry) { 490 const visibleImage = !imageReady && previewReady && !els.lightboxPreview?.hidden ? els.lightboxPreview : els.lightboxImage; 491 const rect = visibleImage.getBoundingClientRect(); 492 frameGeometry = { 493 x: rect.left + rect.width / 2 - appliedX, 494 y: rect.top + rect.height / 2 - appliedY, 495 width: rect.width / appliedScale, 496 height: rect.height / appliedScale, 497 stage: els.lightboxStage.getBoundingClientRect() 498 }; 499 } 500 return frameGeometry; 501 } 502 function updateZoomControls() { 503 for (const [button, disabled] of [[els.lightboxZoomOut, lb.scale <= LIGHTBOX_MIN_SCALE], [els.lightboxZoomIn, lb.scale >= LIGHTBOX_MAX_SCALE]]) { 504 if (button && button.getAttribute("aria-disabled") !== String(disabled)) button.setAttribute("aria-disabled", String(disabled)); 505 } 506 if (els.lightboxHint) { 507 const count = (state.galleryItems || []).length; 508 const touch = coarsePointer?.matches; 509 const hint = touch ? count > 1 ? ui("åææå¨ Â· åæç¼©æ¾ · ç¹å»ç®å¤´æç¼©ç¥å¾ç¿»é¡µ", "宿æå · éæç¸®æ¾ · 黿ç®é æç¸®ç¥åç¿»é ", "Drag to pan · Pinch to zoom · Use arrows or thumbnails to browse") : ui("åææå¨ Â· åæç¼©æ¾", "宿æå · éæç¸®æ¾", "Drag to pan · Pinch to zoom") : ui("ç¹å»æ¾å¤§ · Alt ç¹å»ç¼©å° · æ»è½®ç¼©æ¾ · æå¨å¹³ç§» · Esc éåº", "黿æ¾å¤§ · Alt é»æç¸®å° Â· æ»¾è¼ªç¸®æ¾ Â· æå平移 · Esc éåº", "Click to zoom in · Alt-click to zoom out · Scroll to zoom · Drag to pan · Esc to close"); 510 if (els.lightboxHint.textContent !== hint) els.lightboxHint.textContent = hint; 511 } 512 } 513 function lbApply() { 514 cancelTransformFrame(); 515 const transform = `translate(${lb.x}px, ${lb.y}px) scale(${lb.scale})`; 516 for (const image of [els.lightboxImage, els.lightboxPreview].filter(Boolean)) { 517 image.style.transform = transform; 518 image.style.cursor = lb.dragging ? "grabbing" : "grab"; 519 image.classList.toggle("is-zoomed", lb.scale > 1); 520 } 521 els.lightboxStage.style.cursor = lb.dragging ? "grabbing" : "grab"; 522 if (els.lightboxFit) els.lightboxFit.textContent = lb.scale === 1 ? ui("éåå±å¹", "é©åå±å¹", "Fit to screen") : `${Math.round(lb.scale * 100)}%`; 523 appliedX = lb.x; 524 appliedY = lb.y; 525 appliedScale = lb.scale; 526 updateZoomControls(); 527 if (lb.scale > 1.05) sharpenForZoom(); 528 } 529 function scheduleLbApply() { 530 if (transformFrame !== null) return; 531 if (!win?.requestAnimationFrame) { 532 lbApply(); 533 return; 534 } 535 transformFrame = win.requestAnimationFrame(() => { 536 transformFrame = null; 537 lbApply(); 538 }); 539 } 540 function lbReset() { 541 clearGesture(); 542 lb.dragging = false; 543 dragged = false; 544 lb.scale = 1; 545 lb.x = 0; 546 lb.y = 0; 547 lbApply(); 548 } 549 function zoomTo(scale, point) { 550 const next = Math.min(LIGHTBOX_MAX_SCALE, Math.max(LIGHTBOX_MIN_SCALE, scale)); 551 if (next === lb.scale) return; 552 const ratio = next / lb.scale; 553 if (point) { 554 const rect = geometry(); 555 lb.x -= (point.x - rect.x - lb.x) * (ratio - 1); 556 lb.y -= (point.y - rect.y - lb.y) * (ratio - 1); 557 } else { 558 lb.x *= ratio; 559 lb.y *= ratio; 560 } 561 lb.scale = next; 562 scheduleLbApply(); 563 } 564 function zoomStep(direction) { 565 let next = lb.scale * (direction > 0 ? 1.25 : 0.8); 566 if (lb.scale < 1 && next > 1 || lb.scale > 1 && next < 1) next = 1; 567 if (Math.abs(next - 1) < 1e-3) next = 1; 568 zoomTo(next); 569 lbApply(); 570 } 571 function clearGesture() { 572 const ids = [...pointers.keys()]; 573 pointers.clear(); 574 gesture = null; 575 hadPinch = false; 576 lb.dragging = false; 577 for (const id of ids) { 578 if (els.lightboxStage.hasPointerCapture?.(id)) els.lightboxStage.releasePointerCapture(id); 579 } 580 } 581 function capturePointer(id) { 582 try { 583 els.lightboxStage.setPointerCapture?.(id); 584 } catch (error) { 585 if (error.name !== "NotFoundError") throw error; 586 } 587 } 588 function pinchPoints() { 589 const [a, b] = pointers.values(); 590 return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, distance: Math.hypot(a.x - b.x, a.y - b.y) }; 591 } 592 function beginGesture() { 593 if (pointers.size >= 2) { 594 hadPinch = true; 595 dragged = true; 596 gesture = { kind: "pinch", ...pinchPoints() }; 597 for (const id of pointers.keys()) capturePointer(id); 598 } else if (pointers.size === 1) { 599 const [point] = pointers.values(); 600 gesture = { kind: "pan", x: point.x, y: point.y, ox: lb.x, oy: lb.y }; 601 } else gesture = null; 602 lb.dragging = gesture?.kind === "pan"; 603 }
604 function thumbLabel(item, index) { 605 return ui(`æ¥ç第 ${index + 1} å¼ ï¼${item.title || "å¾ç"}`, `æ¥ç第 ${index + 1} å¼µï¼${item.title || "åç"}`, `View image ${index + 1}: ${item.title || "Image"}`); 606 } 607 function thumbAt(index) { 608 return els.lightboxThumbs?.querySelector(`[data-index="${index}"]`) || null; 609 } 610 function thumbWidth(ratio) { 611 return Math.round(Math.max(44, Math.min(160, (LIGHTBOX_THUMB_HEIGHT - 4) * ratio + 4))); 612 } 613 function rebuildThumbOffsets() { 614 thumbOffsets = [0]; 615 for (const width of thumbWidths) thumbOffsets.push(thumbOffsets.at(-1) + width + LIGHTBOX_THUMB_GAP); 616 } 617 function thumbOffset(index) { 618 return compactTray ? thumbOffsets[index] : index * (LIGHTBOX_THUMB_HEIGHT + LIGHTBOX_THUMB_GAP); 619 } 620 function updateThumbWidth(index, ratio) { 621 const width = thumbWidth(ratio); 622 if (thumbWidths[index] === width) return; 623 thumbWidths[index] = width; 624 rebuildThumbOffsets(); 625 if (thumbWindowEnd > thumbWindowStart) { 626 renderThumbWindow(thumbWindowStart, thumbWindowEnd); 627 activeThumb = thumbAt(lb.index); 628 activeThumb?.setAttribute("aria-selected", "true"); 629 } 630 } 631 function createThumb(item, index, total) { 632 const button = document.createElement("button"); 633 button.type = "button"; 634 button.className = "lightbox-thumb"; 635 button.dataset.index = String(index); 636 button.setAttribute("role", "option"); 637 button.setAttribute("aria-label", thumbLabel(item, index)); 638 button.setAttribute("aria-posinset", String(index + 1)); 639 button.setAttribute("aria-setsize", String(total)); 640 button.setAttribute("aria-selected", "false"); 641 button.tabIndex = -1; 642 button.style.width = `${thumbWidths[index]}px`; 643 const image = document.createElement("img"); 644 image.addEventListener("load", () => { 645 if (renderedThumbItems !== state.galleryItems || !image.naturalWidth || !image.naturalHeight) return; 646 updateThumbWidth(index, image.naturalWidth / image.naturalHeight); 647 }); 648 image.src = railSrc(item); 649 image.alt = ""; 650 image.loading = "eager"; 651 image.decoding = "async"; 652 button.append(image); 653 button.addEventListener("click", () => { 654 if (lb.index === index) return; 655 lb.index = index; 656 lbShow(); 657 }); 658 return button; 659 } 660 function renderThumbWindow(start, end) { 661 const items = state.galleryItems || []; 662 const row = els.lightboxThumbs; 663 const focusedIndex = row.contains(document.activeElement) ? Number(document.activeElement.dataset.index) : -1; 664 activeThumb?.setAttribute("aria-selected", "false"); 665 const existing = new Map([...row.querySelectorAll(".lightbox-thumb")].map((button) => [Number(button.dataset.index), button])); 666 const buttons = []; 667 for (let index = start; index < end; index++) { 668 const button = existing.get(index) || createThumb(items[index], index, items.length); 669 button.style.width = `${thumbWidths[index]}px`; 670 buttons.push(button); 671 } 672 const spacer = (from, to) => { 673 const node = document.createElement("span"); 674 node.className = "lightbox-thumb-spacer"; 675 node.setAttribute("aria-hidden", "true"); 676 node.style.flex = `0 0 ${thumbOffset(to) - thumbOffset(from) - LIGHTBOX_THUMB_GAP}px`; 677 return node; 678 }; 679 row.replaceChildren( 680 ...start ? [spacer(0, start)] : [], 681 ...buttons, 682 ...items.length > end ? [spacer(end, items.length)] : [] 683 ); 684 thumbWindowStart = start; 685 thumbWindowEnd = end; 686 activeThumb = null; 687 if (focusedIndex >= 0) (thumbAt(focusedIndex) || row)?.focus({ preventScroll: true }); 688 } 689 function renderLightboxThumbs({ reveal = true } = {}) { 690 const items = state.galleryItems || []; 691 const row = els.lightboxThumbs; 692 if (!row) return; 693 if (items.length < 2) { 694 row.replaceChildren(); 695 renderedThumbItems = null; 696 thumbWidths = []; 697 thumbOffsets = [0]; 698 activeThumb = null; 699 thumbWindowStart = thumbWindowEnd = 0; 700 return; 701 } 702 const changed = renderedThumbItems !== items; 703 if (changed) { 704 renderedThumbItems = items; 705 thumbWidths = items.map((item) => thumbWidth(itemRatio(item))); 706 rebuildThumbOffsets(); 707 row.scrollLeft = 0; 708 row.scrollTop = 0; 709 thumbWindowStart = thumbWindowEnd = 0; 710 } 711 const virtual = items.length > LIGHTBOX_THUMB_THRESHOLD; 712 const needsWindow = changed || !row.children.length || !virtual && row.children.length !== items.length || virtual && (lb.index < thumbWindowStart + 4 || lb.index >= thumbWindowEnd - 4); 713 if (needsWindow) { 714 const start = virtual ? Math.max(0, Math.min(items.length - LIGHTBOX_THUMB_WINDOW, lb.index - Math.floor(LIGHTBOX_THUMB_WINDOW / 2))) : 0; 715 renderThumbWindow(start, virtual ? start + LIGHTBOX_THUMB_WINDOW : items.length); 716 } 717 const active = thumbAt(lb.index); 718 if (activeThumb !== active) { 719 activeThumb?.setAttribute("aria-selected", "false"); 720 active?.setAttribute("aria-selected", "true"); 721 activeThumb = active; 722 } 723 if (reveal && !row.hidden && active) { 724 const start = compactTray ? active.offsetLeft - row.offsetLeft : active.offsetTop - row.offsetTop; 725 const position = compactTray ? row.scrollLeft : row.scrollTop; 726 const size = compactTray ? active.offsetWidth : active.offsetHeight; 727 const viewport = compactTray ? row.clientWidth : row.clientHeight; 728 if (start < position) row[compactTray ? "scrollLeft" : "scrollTop"] = start; 729 else if (start + size > position + viewport) row[compactTray ? "scrollLeft" : "scrollTop"] = start + size - viewport; 730 } 731 } 732 function itemRatio(item) { 733 const ratio = Number(item?.ratio); 734 if (ratio > 0 && Number.isFinite(ratio)) return ratio; 735 if (typeof item?.ratio === "string" && /[/:]/.test(item.ratio)) { 736 const [left, right] = item.ratio.split(/[/:]/).map(Number); 737 if (left > 0 && right > 0) return left / right; 738 }
739 const width = Number(item?.width || item?.imageWidth); 740 const height = Number(item?.height || item?.imageHeight); 741 return width > 0 && height > 0 ? width / height : 1; 742 } 743 function updateDockSpace() { 744 if (els.lightbox.hidden) return; 745 if (els.lightboxDock) els.lightbox.style.setProperty("--viewer-dock-height", `${Math.ceil(els.lightboxDock.getBoundingClientRect().height)}px`); 746 frameGeometry = null; 747 resizePlaceholder(); 748 } 749 function resizePlaceholder() { 750 if (!els.lightboxPlaceholder || !els.lightboxStage) return; 751 const stage = els.lightboxStage; 752 const style = win.getComputedStyle?.(stage); 753 const width = Math.max(0, stage.clientWidth - (parseFloat(style?.paddingLeft) || 0) - (parseFloat(style?.paddingRight) || 0)); 754 const height = Math.max(0, stage.clientHeight - (parseFloat(style?.paddingTop) || 0) - (parseFloat(style?.paddingBottom) || 0)); 755 const fittedWidth = Math.min(width, height * placeholderRatio); 756 const fittedHeight = fittedWidth / placeholderRatio; 757 if (fittedSize && fittedSize.width && fittedSize.height) { 758 lb.x *= fittedWidth / fittedSize.width; 759 lb.y *= fittedHeight / fittedSize.height; 760 } 761 fittedSize = { width: fittedWidth, height: fittedHeight }; 762 els.lightboxPlaceholder.style.width = `${Math.round(fittedWidth)}px`; 763 els.lightboxPlaceholder.style.height = `${Math.round(fittedHeight)}px`; 764 for (const image of [els.lightboxImage, els.lightboxPreview].filter(Boolean)) { 765 image.style.width = `${Math.round(fittedWidth)}px`; 766 image.style.height = `${Math.round(fittedHeight)}px`; 767 } 768 const full = els.lightboxImage; 769 if (full?.getAttribute("srcset")) { 770 const sizes = fittedSizes(lb.scale); 771 if (Number.parseInt(sizes, 10) > Number.parseInt(full.getAttribute("sizes") || "0", 10)) full.setAttribute("sizes", sizes); 772 } 773 frameGeometry = null; 774 } 775 function setImageStatus(message = "") { 776 if (!els.lightboxImageStatus) return; 777 els.lightboxImageStatus.hidden = !message; 778 if (els.lightboxImageStatusText) els.lightboxImageStatusText.textContent = message; 779 } 780 function srcsetUrls(image) { 781 return String(image.getAttribute("srcset") || "").split(",").map((entry) => entry.trim().split(/\s+/)[0]).filter(Boolean).map((url) => { 782 try { 783 return new URL(url, document.baseURI).href; 784 } catch { 785 return ""; 786 } 787 }); 788 } 789 function displaySourcesFor(item) { 790 return displayImageSources(item?.thumb || item?.src, { ratio: item?.ratio, memberGrant: Boolean(state?.me?.media_display) }); 791 } 792 function railSrc(item) { 793 const display = displaySourcesFor(item); 794 return display ? display.srcset.split(",")[0].trim().split(/\s+/)[0] : item?.thumb || item?.src; 795 } 796 function naturalWidthOf(item) { 797 const match = String(item?.ratio || "").match(/^\s*(\d+(?:\.\d+)?)\s*[/:]/); 798 const width = Number(match?.[1]); 799 return width >= 200 ? width : 0; 800 } 801 function fittedSizes(scale = 1) { 802 const width = Math.max(20, Math.ceil((fittedSize?.width || 720) * Math.max(1, scale) / 20) * 20); 803 return `${Math.min(width, 4e3)}px`; 804 } 805 function sharpenForZoom() { 806 const current = displayState; 807 const full = els.lightboxImage; 808 if (!current || current.generation !== imageGeneration || !imageReady || !full) return; 809 if (full.getAttribute("srcset")) { 810 const sizes = fittedSizes(lb.scale); 811 if (Number.parseInt(sizes, 10) > Number.parseInt(full.getAttribute("sizes") || "0", 10)) full.setAttribute("sizes", sizes); 812 } 813 const maxTier = DISPLAY_IMAGE_WIDTHS[DISPLAY_IMAGE_WIDTHS.length - 1]; 814 const neededPixels = (fittedSize?.width || 0) * lb.scale * (win?.devicePixelRatio || 1); 815 if (current.upgrading || !current.original || !/\/api\/object\/data\/full\//.test(current.original)) return; 816 if (current.naturalWidth && current.naturalWidth <= maxTier || neededPixels <= maxTier) return; 817 current.upgrading = true; 818 const loader = new win.Image(); 819 loader.decoding = "async"; 820 loader.onload = () => { 821 const decoded = typeof loader.decode === "function" ? loader.decode().catch(() => { 822 }) : Promise.resolve(); 823 decoded.then(() => { 824 if (displayState !== current || current.generation !== imageGeneration) return; 825 full.removeAttribute("srcset"); 826 full.removeAttribute("sizes"); 827 full.src = current.original; 828 }); 829 }; 830 loader.onerror = () => { 831 }; 832 loader.src = current.original; 833 } 834 function safePreviewSrc(item) { 835 const thumb = String(item?.thumb || ""); 836 if (!thumb) return ""; 837 try { 838 const url = new URL(thumb, document.baseURI); 839 return url.origin === win.location.origin || url.protocol === "blob:" ? thumb : ""; 840 } catch { 841 return ""; 842 } 843 } 844 function matchesImageSource(image, source) { 845 const assigned = image.getAttribute("src"); 846 if (!assigned) return false;
847 try { 848 const assignedUrl = new URL(assigned, document.baseURI).href; 849 const sourceUrl = new URL(source, document.baseURI).href; 850 const routedUrl = new URL(cdnMediaUrl(source), document.baseURI).href; 851 if (assignedUrl !== sourceUrl && assignedUrl !== routedUrl) return false; 852 if (!image.currentSrc) return true; 853 const current = new URL(image.currentSrc, document.baseURI).href; 854 return current === assignedUrl || srcsetUrls(image).includes(current); 855 } catch { 856 return false; 857 } 858 } 859 function setLightboxImage(item, { preservePreview = false } = {}) { 860 const generation = ++imageGeneration; 861 frameGeometry = null; 862 const full = els.lightboxImage; 863 const preview = els.lightboxPreview; 864 const placeholder = els.lightboxPlaceholder; 865 const original = String(item?.src || ""); 866 const display = displaySourcesFor(item); 867 const source = display ? display.src : original; 868 const thumb = String(item?.loadedSrc || "") || (display ? "" : safePreviewSrc(item)); 869 const retainedPreview = preservePreview && previewReady && preview && !preview.hidden && matchesImageSource(preview, thumb); 870 imageReady = false; 871 previewReady = Boolean(retainedPreview); 872 imageFailed = false; 873 placeholderRatio = itemRatio(item); 874 fittedSize = null; 875 resizePlaceholder(); 876 if (placeholder) placeholder.hidden = Boolean(retainedPreview); 877 setImageStatus(); 878 full.classList.remove("is-ready"); 879 full.onload = null; 880 full.onerror = null; 881 full.removeAttribute("src"); 882 full.removeAttribute("srcset"); 883 full.removeAttribute("sizes"); 884 displayState = display ? { generation, original, naturalWidth: naturalWidthOf(item), upgrading: false } : null; 885 if (preview) { 886 preview.onload = null; 887 preview.onerror = null; 888 if (!retainedPreview) preview.removeAttribute("src"); 889 preview.hidden = !thumb; 890 if (thumb && !retainedPreview) { 891 preview.onload = () => { 892 if (generation !== imageGeneration || !matchesImageSource(preview, thumb)) return; 893 previewReady = true; 894 if (preview.naturalWidth && preview.naturalHeight) placeholderRatio = preview.naturalWidth / preview.naturalHeight; 895 resizePlaceholder(); 896 frameGeometry = null; 897 scheduleLbApply(); 898 if (placeholder) placeholder.hidden = true; 899 if (imageFailed) setImageStatus(ui("髿¸ å¾å 载失败ï¼å·²æ¾ç¤ºé¢è§å¾", "髿¸ åè¼å ¥å¤±æï¼å·²é¡¯ç¤ºé 覽å", "Full image unavailable. Preview shown.")); 900 }; 901 preview.onerror = () => { 902 if (generation !== imageGeneration || !matchesImageSource(preview, thumb)) return; 903 preview.hidden = true; 904 if (imageFailed) setImageStatus(ui("å¾çå 载失败", "åçè¼å ¥å¤±æ", "Image unavailable")); 905 }; 906 preview.src = thumb; 907 } 908 } 909 if (!source) { 910 imageFailed = true; 911 setImageStatus(ui("å¾çå 载失败", "åçè¼å ¥å¤±æ", "Image unavailable")); 912 return; 913 } 914 let expected = source; 915 const finish = () => { 916 if (generation !== imageGeneration || els.lightbox.hidden || imageReady || !matchesImageSource(full, expected)) return; 917 imageReady = true; 918 if (full.naturalWidth && full.naturalHeight) { 919 placeholderRatio = full.naturalWidth / full.naturalHeight; 920 resizePlaceholder(); 921 } 922 full.classList.add("is-ready"); 923 if (preview) preview.hidden = true; 924 if (placeholder) placeholder.hidden = true; 925 setImageStatus(); 926 frameGeometry = null; 927 scheduleLbApply(); 928 }; 929 full.onload = () => { 930 if (generation !== imageGeneration || !matchesImageSource(full, expected)) return; 931 const decoded = typeof full.decode === "function" ? full.decode().catch(() => { 932 }) : Promise.resolve(); 933 decoded.then(finish); 934 }; 935 full.onerror = () => { 936 if (generation !== imageGeneration || !matchesImageSource(full, expected)) return; 937 if (display && expected !== original && original) { 938 expected = original; 939 if (displayState?.generation === generation) displayState = null; 940 full.removeAttribute("srcset"); 941 full.removeAttribute("sizes"); 942 full.src = original; 943 return; 944 } 945 imageFailed = true; 946 setImageStatus(previewReady ? ui("髿¸ å¾å 载失败ï¼å·²æ¾ç¤ºé¢è§å¾", "髿¸ åè¼å ¥å¤±æï¼å·²é¡¯ç¤ºé 覽å", "Full image unavailable. Preview shown.") : thumb ? ui("髿¸ å¾å è½½å¤±è´¥ï¼æ£å¨å è½½é¢è§å¾", "髿¸ åè¼å ¥å¤±æï¼æ£å¨è¼å ¥é 覽å", "Full image unavailable. Loading previewâ¦") : ui("å¾çå 载失败", "åçè¼å ¥å¤±æ", "Image unavailable")); 947 }; 948 if (display) { 949 full.setAttribute("sizes", fittedSizes()); 950 full.setAttribute("srcset", display.srcset); 951 } 952 full.src = source;
953 if (full.complete && full.naturalWidth > 0) full.onload(); 954 } 955 function lbShow() { 956 const items = state.galleryItems || []; 957 const item = items[lb.index]; 958 if (!item) return; 959 lbReset(); 960 metaOpen = false; 961 metaPeek = false; 962 setLightboxImage(item); 963 els.lightboxImage.alt = item.title || ""; 964 const multi = items.length > 1; 965 if (els.lightboxNavigation) els.lightboxNavigation.hidden = !multi; 966 if (els.lightboxPrev) els.lightboxPrev.hidden = !multi; 967 if (els.lightboxNext) els.lightboxNext.hidden = !multi; 968 if (els.lightboxCount) { 969 els.lightboxCount.hidden = !multi; 970 els.lightboxCount.textContent = multi ? `${lb.index + 1} / ${items.length}` : ""; 971 } 972 renderLightboxTopics(item); 973 renderLightboxTags(item); 974 if (els.lightboxTopics) els.lightboxTopics.scrollLeft = 0; 975 renderLightboxThumbs(); 976 } 977 function updateDetailGalleryItem(index, values) { 978 const gallery = transientGallery ? transientPreviousGallery : state.galleryItems; 979 const item = gallery?.[index]; 980 if (!item) return; 981 const previousSrc = item.src;
982 Object.assign(item, values); 983 if (transientGallery || els.lightbox?.hidden) return; 984 if (lb.index === index) { 985 if (item.src !== previousSrc) setLightboxImage(item); 986 els.lightboxImage.alt = item.title || ""; 987 if (Object.hasOwn(values, "sampleTopics")) renderLightboxTopics(item); 988 if (Object.hasOwn(values, "tags")) renderLightboxTags(item); 989 } 990 const button = thumbAt(index); 991 const thumb = button?.querySelector("img"); 992 if (thumb && thumb.getAttribute("src") !== railSrc(item)) thumb.src = railSrc(item); 993 if (button && Object.hasOwn(values, "title")) button.setAttribute("aria-label", thumbLabel(item, index)); 994 if (renderedThumbItems === gallery && (Object.hasOwn(values, "ratio") || Object.hasOwn(values, "width") || Object.hasOwn(values, "height"))) updateThumbWidth(index, itemRatio(item)); 995 } 996 function restoreTransientGallery({ restoreFocus = false } = {}) { 997 const focusTarget = returnFocusTarget; 998 if (transientGallery && state.galleryItems === transientGallery) { 999 state.galleryItems = transientPreviousGallery || []; 1000 } 1001 transientGallery = null; 1002 transientPreviousGallery = null; 1003 returnFocusTarget = null; 1004 if (restoreFocus && focusTarget?.isConnected) focusTarget.focus?.({ preventScroll: true }); 1005 } 1006 function openLightbox(index, options = {}) { 1007 const suppliedItems = Array.isArray(options?.items) ? options.items.filter(Boolean) : null; 1008 if (!els.lightbox || suppliedItems && !suppliedItems.length) return false; 1009 restoreTransientGallery(); 1010 const items = suppliedItems || state.galleryItems || []; 1011 if (!items.length) return false; 1012 if (suppliedItems) { 1013 transientPreviousGallery = state.galleryItems || []; 1014 transientGallery = suppliedItems; 1015 state.galleryItems = suppliedItems; 1016 } 1017 returnFocusTarget = options?.returnFocus || document.activeElement; 1018 compactTray = isCompactTray(); 1019 trayMode = defaultTrayMode(); 1020 metaOpen = false; 1021 metaPeek = false; 1022 els.lightbox.setAttribute("aria-label", ui("大å¾é¢è§", "大åé 覽", "Large image viewer")); 1023 els.lightboxClose?.setAttribute("aria-label", ui("å ³é大徿¥çå¨ (Esc)", "ééå¤§åæª¢è¦å¨ (Esc)", "Close image viewer (Esc)")); 1024 els.lightboxClose?.setAttribute("title", ui("å ³é大徿¥çå¨", "ééå¤§åæª¢è¦å¨", "Close image viewer")); 1025 els.lightboxPrev?.setAttribute("aria-label", ui("ä¸ä¸å¼ ", "ä¸ä¸å¼µ", "Previous image")); 1026 els.lightboxNext?.setAttribute("aria-label", ui("ä¸ä¸å¼ ", "ä¸ä¸å¼µ", "Next image")); 1027 els.lightboxThumbs?.setAttribute("aria-label", ui("åç»å¾ç缩ç¥å¾", "åçµåç縮ç¥å", "Gallery thumbnails")); 1028 els.lightboxThumbToggle?.setAttribute("aria-label", ui("æ¾ç¤ºç¼©ç¥å¾", "顯示縮ç¥å", "Show thumbnails")); 1029 if (els.lightboxThumbToggle) els.lightboxThumbToggle.textContent = ui("缩ç¥å¾", "縮ç¥å", "Thumbnails"); 1030 if (els.lightboxRetry) els.lightboxRetry.textContent = ui("éè¯", "é試", "Retry"); 1031 els.lightboxZoomControls?.setAttribute("aria-label", ui("å¾ç缩æ¾", "åç縮æ¾", "Image zoom")); 1032 for (const [control, label] of [ 1033 [els.lightboxZoomOut, ui("ç¼©å° (â)", "ç¸®å° (â)", "Zoom out (â)")], 1034 [els.lightboxZoomIn, ui("æ¾å¤§ (+)", "æ¾å¤§ (+)", "Zoom in (+)")], 1035 [els.lightboxFit, ui("éåå±å¹ (0)", "é©åå±å¹ (0)", "Fit to screen (0)")] 1036 ]) { 1037 control?.setAttribute("aria-label", label); 1038 control?.setAttribute("title", label); 1039 } 1040 els.lightboxImage.setAttribute("title", ui("ç¹å»æ¾å¤§ · Alt ç¹å»ç¼©å° · æ»è½®ç¼©æ¾", "黿æ¾å¤§ · Alt é»æç¸®å° Â· 滾輪縮æ¾", "Click to zoom in · Alt-click to zoom out · Scroll to zoom")); 1041 if (els.lightboxTagsHeading) els.lightboxTagsHeading.textContent = ui("å½åæ ·å¼ æ ç¾", "ç¶å樣張æ¨ç±¤", "Current image tags"); 1042 if (els.lightboxTopicsHeading) els.lightboxTopicsHeading.textContent = ui("å¾çæå±æ ç®", "åçæå±¬æ¬ç®", "Image categories"); 1043 if (els.lightboxLocate) els.lightboxLocate.textContent = ui("å®ä½å½åå¾", "å®ä½ç¶åå", "Find current image"); 1044 els.lightboxPreview?.setAttribute("title", els.lightboxImage.getAttribute("title")); 1045 renderedThumbItems = null; 1046 lb.index = (index + items.length) % items.length; 1047 els.lightbox.hidden = false;
1048 document.body.classList.add("is-lightbox-open"); 1049 if (!suppliedItems && borrowDetailPanel()) syncTray(); 1050 else { 1051 if (detailHome) setDetailExpanded(false); 1052 els.lightbox.classList.remove("is-detail-expanded"); 1053 els.lightboxDetailSlot.hidden = true; 1054 els.lightboxDetailSlot.inert = true; 1055 if (els.lightboxPanelToggle) els.lightboxPanelToggle.hidden = true; 1056 } 1057 stageObserver?.observe(els.lightboxStage); 1058 if (els.lightboxDock) stageObserver?.observe(els.lightboxDock); 1059 lbShow(); 1060 updateDockSpace(); 1061 revealControls(); 1062 els.lightboxClose?.focus(); 1063 return true; 1064 } 1065 function closeLightbox({ restoreFocus = true } = {}) { 1066 if (!els.lightbox || els.lightbox.hidden) return false; 1067 els.lightbox.hidden = true; 1068 if (controlTimer !== null) win.clearTimeout(controlTimer); 1069 controlTimer = null; 1070 els.lightbox.classList.remove("is-controls-visible"); 1071 stageObserver?.disconnect(); 1072 cancelTransformFrame(); 1073 clearGesture(); 1074 dragged = false; 1075 imageGeneration++; 1076 imageReady = false; 1077 previewReady = false; 1078 imageFailed = false; 1079 document.body.classList.remove("is-lightbox-open"); 1080 returnDetailPanel(); 1081 els.lightboxImage.onload = null; 1082 els.lightboxImage.onerror = null; 1083 els.lightboxImage.removeAttribute("src"); 1084 els.lightboxImage.classList.remove("is-ready", "is-zoomed"); 1085 if (els.lightboxPreview) { 1086 els.lightboxPreview.onload = null; 1087 els.lightboxPreview.onerror = null; 1088 els.lightboxPreview.removeAttribute("src"); 1089 } 1090 setImageStatus(); 1091 if (thumbScrollFrame !== null) win.cancelAnimationFrame(thumbScrollFrame); 1092 thumbScrollFrame = null; 1093 els.lightboxThumbs?.replaceChildren(); 1094 els.lightboxTopics?.replaceChildren(); 1095 els.lightboxTags?.replaceChildren(); 1096 renderedThumbItems = null; 1097 thumbWidths = []; 1098 thumbOffsets = [0]; 1099 activeThumb = null; 1100 thumbWindowStart = thumbWindowEnd = 0; 1101 renderedTopicsKey = null; 1102 renderedTagsKey = null; 1103 topicCount = 0; 1104 trayMode = "closed"; 1105 metaOpen = false; 1106 metaPeek = false; 1107 fittedSize = null; 1108 if (els.lightboxTopics) els.lightboxTopics.hidden = true; 1109 if (els.lightboxTags) els.lightboxTags.hidden = true; 1110 restoreTransientGallery({ restoreFocus }); 1111 return true; 1112 } 1113 function stepLightbox(dir) { 1114 const items = state.galleryItems || []; 1115 if (items.length < 2) return; 1116 lb.index = (lb.index + dir + items.length) % items.length; 1117 lbShow(); 1118 } 1119 function revealControls() { 1120 if (els.lightbox.hidden) return; 1121 els.lightbox.classList.add("is-controls-visible"); 1122 if (controlTimer !== null) win.clearTimeout(controlTimer); 1123 controlTimer = win.setTimeout(() => { 1124 controlTimer = null; 1125 if (!els.lightboxDock?.contains(document.activeElement) && !els.lightboxDock?.matches(":hover")) { 1126 els.lightbox.classList.remove("is-controls-visible"); 1127 } 1128 }, 2e3); 1129 } 1130 function installLightboxListeners() { 1131 if (!els.lightbox) return; 1132 els.lightboxStage.addEventListener("wheel", (event) => { 1133 if (els.lightbox.hidden) return; 1134 revealControls(); 1135 event.preventDefault(); 1136 const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? els.lightboxStage.clientHeight : 1; 1137 const wheelDelta = Math.max(-120, Math.min(120, event.deltaY * unit)); 1138 zoomTo(lb.scale * Math.exp(-wheelDelta * (event.ctrlKey ? 0.01 : LIGHTBOX_WHEEL_ZOOM_INTENSITY)), { x: event.clientX, y: event.clientY }); 1139 }, { passive: false }); 1140 const clickImage = (event) => { 1141 if (els.lightbox.hidden || dragged || hadPinch || event.detail > 1 || lastImagePointerType !== "mouse") return; 1142 const direction = event.altKey ? 1 / 1.6 : 1.6; 1143 zoomTo(lb.scale * direction, { x: event.clientX, y: event.clientY }); 1144 }; 1145 els.lightboxImage.addEventListener("click", clickImage); 1146 els.lightboxPreview?.addEventListener("click", clickImage); 1147 els.lightboxStage.addEventListener("pointerdown", (event) => { 1148 dragged = false; 1149 if (els.lightbox.hidden || event.button !== 0) return; 1150 revealControls(); 1151 if (event.target === els.lightboxImage || event.target === els.lightboxPreview) lastImagePointerType = event.pointerType || "mouse"; 1152 if (!pointers.size) hadPinch = false;
1153 pointers.set(event.pointerId, { x: event.clientX, y: event.clientY, touch: event.pointerType === "touch" }); 1154 beginGesture(); 1155 scheduleLbApply(); 1156 }); 1157 els.lightboxStage.addEventListener("pointermove", (event) => { 1158 if (event.pointerType === "mouse" || event.pointerType === "pen") revealControls(); 1159 if (!pointers.has(event.pointerId) || !gesture) return; 1160 const point = { x: event.clientX, y: event.clientY, touch: event.pointerType === "touch" }; 1161 pointers.set(event.pointerId, point); 1162 if (gesture.kind === "pinch") { 1163 const next = pinchPoints(); 1164 if (gesture.distance > 0) zoomTo(lb.scale * next.distance / gesture.distance, gesture); 1165 lb.x += next.x - gesture.x; 1166 lb.y += next.y - gesture.y; 1167 gesture = { kind: "pinch", ...next }; 1168 scheduleLbApply(); 1169 return; 1170 } 1171 if (!dragged && Math.hypot(point.x - gesture.x, point.y - gesture.y) > 3) { 1172 dragged = true; 1173 capturePointer(event.pointerId); 1174 } 1175 if (!dragged || gesture.kind !== "pan") return; 1176 lb.x = gesture.ox + (point.x - gesture.x); 1177 lb.y = gesture.oy + (point.y - gesture.y); 1178 scheduleLbApply(); 1179 }); 1180 const endDrag = (event) => { 1181 if (!pointers.has(event.pointerId)) return; 1182 pointers.delete(event.pointerId); 1183 beginGesture(); 1184 if (els.lightboxStage.hasPointerCapture?.(event.pointerId)) els.lightboxStage.releasePointerCapture(event.pointerId); 1185 lbApply(); 1186 }; 1187 els.lightboxStage.addEventListener("pointerup", endDrag); 1188 els.lightboxStage.addEventListener("pointercancel", endDrag); 1189 els.lightboxStage.addEventListener("lostpointercapture", (event) => { 1190 if (event.target === els.lightboxStage && !els.lightboxStage.hasPointerCapture?.(event.pointerId)) endDrag(event); 1191 }); 1192 els.lightboxStage.addEventListener("dblclick", (event) => { 1193 if (event.target.closest?.("button")) return; 1194 if (lb.scale >= 2.5) lbReset(); 1195 else { 1196 zoomTo(2.5, { x: event.clientX, y: event.clientY }); 1197 lbApply(); 1198 } 1199 }); 1200 els.lightboxStage.addEventListener("click", (event) => { 1201 if (event.pointerType !== "touch" || event.target !== els.lightboxStage || dragged) return; 1202 if (els.lightbox.classList.contains("is-controls-visible")) els.lightbox.classList.remove("is-controls-visible"); 1203 else revealControls(); 1204 }); 1205 els.lightboxDock?.addEventListener("focusin", revealControls); 1206 els.lightboxDock?.addEventListener("pointermove", revealControls); 1207 els.lightboxClose?.addEventListener("click", closeLightbox); 1208 els.lightboxPanelToggle?.addEventListener("click", () => setDetailExpanded(!detailExpanded)); 1209 els.lightboxLocate?.addEventListener("click", () => { 1210 els.lightboxTray?.scrollIntoView({ block: "start", behavior: "smooth" }); 1211 renderLightboxThumbs(); 1212 }); 1213 els.lightboxZoomOut?.addEventListener("click", () => zoomStep(-1)); 1214 els.lightboxZoomIn?.addEventListener("click", () => zoomStep(1)); 1215 els.lightboxFit?.addEventListener("click", lbReset); 1216 els.lightboxRetry?.addEventListener("click", () => { 1217 const item = state.galleryItems?.[lb.index]; 1218 if (item) setLightboxImage(item, { preservePreview: true }); 1219 }); 1220 els.lightboxThumbToggle?.addEventListener("click", () => { 1221 trayMode = trayMode === "thumbs" ? "closed" : "thumbs"; 1222 syncTray(); 1223 if (trayMode === "thumbs") renderLightboxThumbs(); 1224 }); 1225 els.lightboxTopicsToggle?.addEventListener("click", () => { 1226 metaOpen = !metaOpen; 1227 syncTray(); 1228 }); 1229 els.lightboxThumbs?.addEventListener("pointerover", (event) => { 1230 if (!event.target.closest?.('.lightbox-thumb[aria-selected="true"]')) return; 1231 metaPeek = true; 1232 syncTray(); 1233 }); 1234 els.lightboxTray?.addEventListener("pointerleave", () => { 1235 if (metaPeek) { 1236 metaPeek = false; 1237 syncTray(); 1238 } 1239 }); 1240 els.lightboxThumbs?.addEventListener("focusin", (event) => { 1241 if (!event.target.closest?.('.lightbox-thumb[aria-selected="true"]')) return; 1242 metaPeek = true; 1243 syncTray(); 1244 }); 1245 els.lightboxTray?.addEventListener("focusout", (event) => { 1246 if (metaPeek && !els.lightboxTray.contains(event.relatedTarget)) { 1247 metaPeek = false;
1248 syncTray(); 1249 } 1250 }); 1251 els.lightboxThumbs?.addEventListener("keydown", (event) => { 1252 const forward = compactTray ? "ArrowRight" : "ArrowDown"; 1253 const backward = compactTray ? "ArrowLeft" : "ArrowUp"; 1254 if (![forward, backward, "Home", "End"].includes(event.key)) return; 1255 event.preventDefault(); 1256 event.stopPropagation(); 1257 const items = state.galleryItems || []; 1258 if (items.length < 2) return; 1259 const focusIndex = Number(event.target.closest?.("[data-index]")?.dataset.index); 1260 const origin = Number.isFinite(focusIndex) ? focusIndex : lb.index; 1261 const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (origin + (event.key === forward ? 1 : -1) + items.length) % items.length; 1262 if (next !== lb.index) { 1263 lb.index = next; 1264 lbShow(); 1265 } 1266 thumbAt(next)?.focus({ preventScroll: true }); 1267 }); 1268 els.lightboxThumbs?.addEventListener("scroll", () => { 1269 if (thumbScrollFrame !== null || renderedThumbItems?.length <= LIGHTBOX_THUMB_THRESHOLD) return; 1270 thumbScrollFrame = win.requestAnimationFrame(() => { 1271 thumbScrollFrame = null; 1272 const row = els.lightboxThumbs; 1273 if (els.lightbox.hidden || row.hidden) return; 1274 const position = compactTray ? row.scrollLeft + row.clientWidth / 2 : row.scrollTop + row.clientHeight / 2; 1275 let low = 0, high = renderedThumbItems.length; 1276 while (low < high) { 1277 const middle2 = low + high >>> 1; 1278 if (thumbOffset(middle2 + 1) <= position) low = middle2 + 1; 1279 else high = middle2; 1280 } 1281 const middle = Math.min(renderedThumbItems.length - 1, low); 1282 if (middle >= thumbWindowStart + 5 && middle < thumbWindowEnd - 5) return; 1283 const start = Math.max(0, Math.min(renderedThumbItems.length - LIGHTBOX_THUMB_WINDOW, middle - Math.floor(LIGHTBOX_THUMB_WINDOW / 2))); 1284 if (start === thumbWindowStart) return; 1285 renderThumbWindow(start, start + LIGHTBOX_THUMB_WINDOW); 1286 const current = thumbAt(lb.index); 1287 current?.setAttribute("aria-selected", "true"); 1288 activeThumb = current; 1289 }); 1290 }, { passive: true }); 1291 const resized = (entries) => { 1292 if (els.lightbox.hidden) return; 1293 if (Array.isArray(entries) && entries.every((entry) => entry.target === els.lightboxDock)) { 1294 updateDockSpace(); 1295 return; 1296 } 1297 frameGeometry = null; 1298 clearGesture(); 1299 const nextCompact = isCompactTray(); 1300 if (compactTray !== nextCompact) { 1301 compactTray = nextCompact; 1302 trayMode = defaultTrayMode(); 1303 if (detailHome) setDetailExpanded(!compactTray); 1304 if (thumbWindowEnd > thumbWindowStart) renderThumbWindow(thumbWindowStart, thumbWindowEnd); 1305 els.lightboxThumbs.scrollLeft = 0; 1306 els.lightboxThumbs.scrollTop = 0; 1307 } 1308 syncTray(); 1309 updateDockSpace(); 1310 scheduleLbApply(); 1311 }; 1312 if (win?.ResizeObserver) stageObserver = new win.ResizeObserver(resized); 1313 win?.addEventListener("resize", resized); 1314 win?.addEventListener("blur", () => { 1315 if (els.lightbox.hidden) return; 1316 clearGesture(); 1317 lbApply(); 1318 }); 1319 els.lightboxPrev?.addEventListener("click", () => stepLightbox(-1)); 1320 els.lightboxNext?.addEventListener("click", () => stepLightbox(1)); 1321 document.addEventListener("keydown", (event) => { 1322 if (els.lightbox.hidden || event.defaultPrevented) return; 1323 if (event.key === "Escape") { 1324 event.preventDefault(); 1325 event.stopImmediatePropagation(); 1326 if (metaOpen) { 1327 metaOpen = false; 1328 syncTray(); 1329 els.lightboxTopicsToggle?.focus(); 1330 } else if (trayMode === "thumbs") { 1331 trayMode = "closed"; 1332 syncTray(); 1333 els.lightboxThumbToggle?.focus(); 1334 } else closeLightbox(); 1335 } else if (event.key === "Tab") { 1336 const controls = [...els.lightbox.querySelectorAll('button, a[href], [tabindex="0"]')].filter((node) => !node.disabled && !node.closest("[hidden], [inert]") && win.getComputedStyle(node).visibility !== "hidden"); 1337 const first = controls[0], last = controls.at(-1);
1338 const outside = !els.lightbox.contains(document.activeElement); 1339 if (event.shiftKey && (document.activeElement === first || outside)) { 1340 event.preventDefault(); 1341 last?.focus(); 1342 } else if (!event.shiftKey && (document.activeElement === last || outside)) { 1343 event.preventDefault(); 1344 first?.focus(); 1345 } 1346 } else if (event.ctrlKey || event.metaKey || event.altKey || event.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return; 1347 else if (event.key === "+" || event.key === "=") { 1348 event.preventDefault(); 1349 zoomStep(1); 1350 } else if (event.key === "-" || event.key === "_") { 1351 event.preventDefault(); 1352 zoomStep(-1); 1353 } else if (event.key === "0") { 1354 event.preventDefault(); 1355 lbReset(); 1356 } else if (els.lightboxTopics?.contains(event.target)) return; 1357 else if (event.key === "ArrowLeft") { 1358 event.preventDefault(); 1359 stepLightbox(-1); 1360 } else if (event.key === "ArrowRight") { 1361 event.preventDefault(); 1362 stepLightbox(1); 1363 } 1364 }); 1365 } 1366 installLightboxListeners(); 1367 return { 1368 openLightbox, 1369 closeLightbox, 1370 stepLightbox, 1371 updateDetailGalleryItem, 1372 renderLightboxThumbs 1373 }; 1374} 1375export { 1376 createLightboxModule 1377};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.