1/** 2 * Comment Barrage Module 3 * ES2022 version with proper error handling and cleanup 4 */ 5 6;(() => { 7 "use strict" 8 9 const state = { 10 swiper: null, 11 scrollHandler: null, 12 abortController: null, 13 generation: 0, 14 hoverElement: null, 15 mouseoverHandler: null, 16 mouseoutHandler: null, 17 timers: [] 18 } 19 20 const config = { 21 lightColors: [["#ffffffaa!important", "var(--lyx-black)"]], 22 darkColors: [["#000a!important", "var(--lyx-white)"]], 23 noAvatarType: "retro", 24 avatarCDN: "cravatar.cn", 25 displayBarrage: true 26 } 27 28 const parseHttpUrl = (value) => { 29 if (typeof value !== "string" || !value.trim()) return null 30 31 try { 32 const url = new URL(value) 33 return url.protocol === "http:" || url.protocol === "https:" ? url.href : null 34 } catch { 35 return null 36 } 37 } 38 39 const isInViewPort = (element) => { 40 if (!element) return false 41 return element.getBoundingClientRect().top <= window.innerHeight 42 } 43 44 const flattenComment = (comment, flattened) => { 45 flattened.push(comment) 46 if (Array.isArray(comment.replies)) { 47 comment.replies.forEach((reply) => flattenComment(reply, flattened)) 48 } 49 } 50 51 const filterComments = (data) => { 52 if (!Array.isArray(data)) return [] 53 54 const flattened = [] 55 data 56 .slice() 57 .sort((a, b) => a.created - b.created) 58 .forEach((comment) => { 59 flattenComment(comment, flattened) 60 }) 61 return flattened 62 } 63 64 const createBarrageElement = (data) => { 65 const barrage = document.createElement("div") 66 barrage.className = "comment-barrage-item" 67 68 const colorIndex = Math.floor(Math.random() * config.lightColors.length) 69 const lightColor = config.lightColors[colorIndex] 70 const darkColor = config.darkColors[colorIndex] 71 const styleElement = document.getElementById("barragesColor") 72 if (styleElement) { 73 styleElement.textContent = ` 74 [data-theme='light'] .comment-barrage-item { 75 background-color: ${lightColor[0]}; 76 color: ${lightColor[1]}; 77 } 78 [data-theme='dark'] .comment-barrage-item { 79 background-color: ${darkColor[0]}; 80 color: ${darkColor[1]}; 81 } 82 ` 83 } 84 85 const nick = String(data.nick ?? "") 86 const fallbackAvatar = `https://${config.avatarCDN}/avatar/${encodeURIComponent(String(data.mailMd5 ?? ""))}?d=${encodeURIComponent(config.noAvatarType)}` 87 const avatarUrl = parseHttpUrl(data.avatar) || fallbackAvatar 88 const link = parseHttpUrl(data.link) 89 90 const head = document.createElement("div") 91 head.className = "barrageHead" 92 93 const avatar = document.createElement("img") 94 avatar.className = "barrageAvatar" 95 avatar.src = avatarUrl 96 avatar.alt = nick 97 head.appendChild(avatar) 98 99 const nickElement = document.createElement(link ? "a" : "div") 100 nickElement.className = "barrageNick" 101 nickElement.textContent = nick 102 if (link) { 103 nickElement.href = link 104 nickElement.target = "_blank" 105 nickElement.rel = "noopener noreferrer" 106 } 107 head.appendChild(nickElement) 108 109 const close = document.createElement("a") 110 close.className = "barrage-close" 111 close.href = "#" 112 close.style.fontSize = "20px" 113 close.textContent = "Ã" 114 close.addEventListener( 115 "click", 116 (event) => { 117 event.preventDefault() 118 barrage.classList.add("out") 119 }, 120 { once: true } 121 ) 122 head.appendChild(close) 123 barrage.appendChild(head) 124 125 const content = document.createElement("a") 126 content.className = "barrageContent" 127 content.href = `#${encodeURIComponent(String(data.id ?? ""))}` 128 content.textContent = String(data.comment ?? "") 129 barrage.appendChild(content) 130 131 const slide = document.createElement("div") 132 slide.className = "swiper-slide" 133 slide.style.height = "150px" 134 slide.appendChild(barrage) 135 return slide 136 } 137 138 const initSwiper = (swiperElement) => { 139 if (typeof Swiper === "undefined") return null 140 141 const swiper = new Swiper(swiperElement, { 142 direction: "vertical", 143 loop: true, 144 mousewheel: true, 145 autoplay: { 146 delay: 3000, 147 stopOnLastSlide: false, 148 disableOnInteraction: false 149 } 150 }) 151 152 state.hoverElement = swiper.el 153 state.mouseoverHandler = () => swiper.autoplay.stop() 154 state.mouseoutHandler = () => swiper.autoplay.start() 155 state.hoverElement.addEventListener("mouseover", state.mouseoverHandler) 156 state.hoverElement.addEventListener("mouseout", state.mouseoutHandler) 157 return swiper 158 } 159 160 const fetchComments = async (endpoint, signal, pathname) => { 161 try { 162 const response = await fetch(endpoint, { 163 method: "POST", 164 headers: { 165 "Content-Type": "application/json" 166 }, 167 body: JSON.stringify({ 168 event: "COMMENT_GET", 169 url: pathname 170 }), 171 signal 172 }) 173 174 if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) 175 const result = await response.json() 176 return filterComments(result.data || []) 177 } catch (error) { 178 if (error.name !== "AbortError") console.error("è·åè¯è®ºå¼¹å¹å¤±è´¥:", error) 179 return [] 180 } 181 } 182 183 const renderBarrages = (comments, barrageContainer, swiperElement) => { 184 barrageContainer.replaceChildren(...comments.map(createBarrageElement)) 185 if (comments.length) state.swiper = initSwiper(swiperElement) 186 } 187 188 const setBarrageVisibility = (barrageContainer, visible, animate = false) => { 189 config.displayBarrage = visible 190 if (!barrageContainer) return 191 192 if (visible) { 193 barrageContainer.style.display = "" 194 barrageContainer.style.opacity = animate ? "0" : "1" 195 if (animate) { 196 barrageContainer.style.transition = "opacity 0.4s" 197 state.timers.push( 198 setTimeout(() => { 199 barrageContainer.style.opacity = "1" 200 }, 10) 201 ) 202 } 203 } else if (animate) { 204 barrageContainer.style.opacity = "0" 205 state.timers.push( 206 setTimeout(() => { 207 barrageContainer.style.display = "none" 208 }, 400) 209 ) 210 } else { 211 barrageContainer.style.display = "none" 212 barrageContainer.style.opacity = "0" 213 } 214 } 215 216 const toggleBarrage = () => { 217 const visible = localStorage.getItem("isBarrageToggle") === "1" 218 localStorage.setItem("isBarrageToggle", visible ? "0" : "1") 219 setBarrageVisibility( 220 document.querySelector(".comment-barrage"), 221 visible, 222 !isInViewPort(document.getElementById("post-comment")) 223 ) 224 } 225 226 const setupScrollHandler = (commentElement, swiperElement) => { 227 state.scrollHandler = () => { 228 if (!config.displayBarrage) return 229 230 if (isInViewPort(commentElement)) { 231 swiperElement.style.transform = "translateX(514px)" 232 swiperElement.style.opacity = "0" 233 } else { 234 swiperElement.style.transform = "" 235 swiperElement.style.opacity = "1" 236 } 237 } 238 lifecycle.onScroll(state.scrollHandler) 239 state.scrollHandler() 240 } 241 242 const cleanup = () => { 243 state.generation += 1 244 state.abortController?.abort() 245 state.abortController = null 246 247 if (state.hoverElement) { 248 state.hoverElement.removeEventListener("mouseover", state.mouseoverHandler) 249 state.hoverElement.removeEventListener("mouseout", state.mouseoutHandler) 250 state.hoverElement = null 251 state.mouseoverHandler = null 252 state.mouseoutHandler = null 253 } 254 255 if (state.swiper?.destroy) state.swiper.destroy(true, true) 256 state.swiper = null 257 258 if (state.scrollHandler) { 259 lifecycle.offScroll(state.scrollHandler) 260 state.scrollHandler = null 261 } 262
263 state.timers.forEach(clearTimeout) 264 state.timers = [] 265 document.querySelector(".comment-barrage")?.replaceChildren() 266 } 267 268 const init = async () => { 269 cleanup() 270 271 const commentElement = document.getElementById("post-comment") 272 const swiperElement = document.querySelector(".barrageswiper") 273 const barrageContainer = swiperElement?.querySelector(".comment-barrage") 274 const endpoint = parseHttpUrl(swiperElement?.dataset.endpoint) 275 if (!commentElement || !swiperElement || !barrageContainer || !endpoint) return 276 277 const barrageVisible = localStorage.getItem("isBarrageToggle") !== "1" 278 if (localStorage.getItem("isBarrageToggle") === null) 279 localStorage.setItem("isBarrageToggle", "0") 280 setBarrageVisibility(barrageContainer, barrageVisible) 281 282 const generation = state.generation 283 const pathname = window.location.pathname 284 state.abortController = new AbortController() 285 const comments = await fetchComments(endpoint, state.abortController.signal, pathname) 286 if (generation !== state.generation || pathname !== window.location.pathname) return 287 288 state.abortController = null 289 renderBarrages(comments, barrageContainer, swiperElement) 290 setupScrollHandler(commentElement, swiperElement) 291 } 292 293 window.switchCommentBarrage = toggleBarrage 294 lifecycle.onReady(init) 295 lifecycle.onCleanup(cleanup) 296})()
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.