PageSourceSearch

https://hoshino-yumetsuki.github.io/js/commentBarrage.js

js hoshino-yumetsuki.github.io collected 2026-10-03 09:18:54 UTC 9,014 bytes, 296 lines download raw bytes

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.