bangumi-comment-enhance

Improve comment reading experience, hide certain comments, sort featured comments by reaction count or reply count, and more.

// ==UserScript==
// @name        bangumi-comment-enhance
// @version     0.2.7
// @description Improve comment reading experience, hide certain comments, sort featured comments by reaction count or reply count, and more.
// @author      Flynn Cao
// @namespace   https://flynncao.uk/
// @match       https://bangumi.tv/*
// @match       https://chii.in/*
// @match       https://bgm.tv/*
// @include     /^https?:\/\/(((fast\.)?bgm\.tv)|chii\.in|bangumi\.tv)*/
// @license     MIT
// ==/UserScript==
'use strict'

class CustomCheckboxContainer {
  constructor(id, label, checked) {
    this.id = id
    this.label = label
    this.checked = checked
    this.input = null
  }

  createElement() {
    if (this.element) {
      return this.element
    }
    const checkbox = document.createElement('input')
    checkbox.type = 'checkbox'
    checkbox.id = this.id
    checkbox.checked = this.checked
    this.input = checkbox
    return checkbox
  }

  createLabel() {
    const label = document.createElement('label')
    label.htmlFor = this.id
    label.textContent = this.label
    return label
  }

  getContainer() {
    const container = document.createElement('div')
    container.className = 'checkbox-container'
    container.append(this.createElement())
    container.append(this.createLabel())
    return container
  }

  getInput() {
    return this.input
  }
}

// https://www.iconfont.cn/collections/detail?spm=a313x.user_detail.i1.dc64b3430.57e63a81itWm4A&cid=12086
const Icons = {
  eyeOpen:
    '<svg t="1747629142037" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1338" width="256" height="256"><path d="M947.6 477.1c-131.1-163.4-276.3-245-435.6-245s-304.5 81.7-435.6 245c-16.4 20.5-16.4 49.7 0 70.1 131.1 163.4 276.3 245 435.6 245s304.5-81.7 435.6-245c16.4-20.4 16.4-49.6 0-70.1zM512 720c-130.6 0-251.1-67.8-363.5-207.8 112.4-140 232.9-207.8 363.5-207.8s251.1 67.8 363.5 207.8C763.1 652.2 642.6 720 512 720z" fill="#333333" p-id="1339"></path><path d="M512 592c44.1 0 79.8-35.7 79.8-79.8 0-44.1-35.7-79.8-79.8-79.8-44.1 0-79.8 35.7-79.8 79.8-0.1 44.1 35.7 79.8 79.8 79.8z m0 72c-83.8 0-151.8-68-151.8-151.8 0-83.8 68-151.8 151.8-151.8 83.8 0 151.8 68 151.8 151.8 0 83.8-68 151.8-151.8 151.8z m0 0" fill="#333333" p-id="1340"></path></svg>',
  newest:
    '<svg t="1747628315444" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1861" width="256" height="256"><path d="M512.736 992a483.648 483.648 0 0 1-164.672-28.8 36.88 36.88 0 1 1 25.104-69.36 407.456 407.456 0 1 0-184.608-136.512A36.912 36.912 0 0 1 129.488 801.6a473.424 473.424 0 0 1-97.472-290A480 480 0 1 1 512.736 992z" fill="#5F5F5F" p-id="1862"></path><path d="M685.6 638.592a32 32 0 0 1-14.032-2.96l-178.048-73.888a36.8 36.8 0 0 1-22.912-34.016V236.672a36.944 36.944 0 1 1 73.888 0v266.72l155.2 64.272a36.336 36.336 0 0 1 19.952 48 37.616 37.616 0 0 1-34.048 22.928z" fill="#5F5F5F" p-id="1863"></path></svg>',
  answerSheet:
    '<svg t="1741855047626" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2040" width="256" height="256"><path d="M188.8 135.7c-29.7 0-53.8 24.1-53.8 53.7v644.7c0 29.7 24.1 53.7 53.8 53.7h645.4c29.7 0 53.8-24.1 53.8-53.7V189.4c0-29.7-24.1-53.7-53.8-53.7H188.8z m-13-71.1h671.5c61.8 0 111.9 50.1 111.9 111.8v670.8c0 61.7-50.1 111.8-111.9 111.8H175.8C114 959 63.9 909 63.9 847.2V176.4c0-61.8 50.1-111.8 111.9-111.8z m0 0" fill="#333333" p-id="2041"></path><path d="M328 328h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM556 332h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM784 332h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#333333" p-id="2042"></path><path d="M328 546h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM556 550h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM784 550h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#333333" p-id="2043"></path><path d="M328 764h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM556 768h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM784 768h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#333333" p-id="2044"></path></svg>',
  sorting:
    '<svg t="1741855109866" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2338" width="256" height="256"><path d="M375 898c-19.8 0-36-16.2-36-36V162c0-19.8 16.2-36 36-36s36 16.2 36 36v700c0 19.8-16.2 36-36 36z" fill="#333333" p-id="2339"></path><path d="M398.2 889.6c-15.2 12.7-38 10.7-50.7-4.4L136.6 633.9c-12.7-15.2-10.7-38 4.4-50.7 15.2-12.7 38-10.7 50.7 4.4l210.8 251.3c12.8 15.2 10.8 38-4.3 50.7zM649 126c19.8 0 36 16.2 36 36v700c0 19.8-16.2 36-36 36s-36-16.2-36-36V162c0-19.8 16.2-36 36-36z" fill="#333333" p-id="2340"></path><path d="M625.8 134.4c15.2-12.7 38-10.7 50.7 4.4l210.8 251.3c12.7 15.2 10.7 38-4.4 50.7-15.2 12.7-38 10.7-50.7-4.4L621.4 185.1c-12.7-15.2-10.7-38 4.4-50.7z" fill="#333333" p-id="2341"></path></svg>',
  font: '<svg t="1741855156691" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2635" width="256" height="256"><path d="M859 201H165c-19.8 0-36-16.2-36-36s16.2-36 36-36h694c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#585757" p-id="2636"></path><path d="M476 859V165c0-19.8 16.2-36 36-36s36 16.2 36 36v694c0 19.8-16.2 36-36 36s-36-16.2-36-36z" fill="#585757" p-id="2637"></path></svg>',
  gear: '<svg t="1741861365461" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2783" data-darkreader-inline-fill="" width="256" height="256"><path d="M594.9 64.8c36.8-0.4 66.9 29.1 67.3 65.9v7.8c0 38.2 31.5 69.4 70.2 69.4 12.3 0 24.5-3.3 35-9.3l7.1-4.1c10.3-5.9 22.1-9 33.9-9 23.9 0 46.2 12.5 58.3 32.8L949.9 359c18.7 31.6 7.6 71.9-24.6 90.1l-6.9 3.9c-34 19.2-45.7 61.2-26.4 93.8 6.1 10.3 14.9 18.9 25.4 24.8l7 3.9c32.3 18 43.6 58.5 24.8 90.2L866 806.3c-9.1 15.2-23.8 26.2-41 30.6-17.1 4.4-35.3 2.2-50.7-6.4l-7-3.9c-21.9-12.2-48.5-12.4-70.6-0.4-10.7 5.9-19.7 14.5-25.9 25-6.1 10.4-9.4 22.1-9.3 33.8v7.8c0.1 17.8-7.2 34.7-20 47.1-12.6 12.2-29.6 19-47.2 19H428c-36.6 0.3-66.7-29-67.2-65.5l-0.1-7.8c-0.1-18.4-7.6-36-20.8-48.8-22.5-22-56.9-26.5-84.3-10.9l-7 4.1c-10.3 5.8-22 8.9-33.8 8.9-23.9 0-46.1-12.4-58.2-32.8L73.2 665.2c-8.9-15.1-11.3-33.2-6.7-50.1 4.6-16.9 15.8-31.3 31.2-39.8l6.8-3.9c16.2-9 28.2-24.2 33.1-42.1 4.9-17.4 2.4-36.1-6.9-51.6-6.2-10.4-15.1-19-25.7-24.9l-6.9-3.9c-15.5-8.4-27-22.8-31.7-39.8-4.7-17-2.3-35.2 6.7-50.4L156.3 218c9-15.1 23.8-26.2 41-30.6 17.1-4.4 35.3-2.1 50.7 6.5l7.1 3.9c21.9 12.3 48.6 12.5 70.7 0.5 10.8-5.9 19.8-14.6 26-25.1 6.1-10.4 9.3-22.2 9.2-34.1v-7.9c-0.2-17.8 7-34.8 19.8-47.2 12.6-12.3 29.7-19.1 47.5-19.1h166.6z m-163.2 71c-3.1 0-6.1 1.2-8.4 3.3-1.9 1.8-2.9 4.2-2.9 6.8l0.1 7.6c0.2 21.2-5.4 42-16.3 60.3a120.02 120.02 0 0 1-45.2 43.7c-37.4 20.4-82.6 20.2-119.7-0.7l-6.8-3.8c-2.8-1.6-6.1-2-9.2-1.2-2.8 0.7-5.3 2.5-6.8 5l-80 135.1c-2.7 4.5-1.1 10.2 4.1 13l6.7 3.7c18.6 10.3 34 25.3 44.7 43.4 16.3 27.6 20.6 59.9 12.1 90.8-8.5 30.8-29 56.9-56.9 72.5l-6.6 3.7c-5 2.9-6.6 8.5-3.9 12.9l80 135.1c1.9 3.2 5.7 5.3 10 5.3 2.1 0 4.3-0.5 6.1-1.6l6.8-3.8c18.1-10.3 38.8-15.8 59.9-15.8 31.8 0 62 12.3 84.7 34.4 23 22.5 35.9 52.6 36 84.7v7.5c0 5.2 4.9 9.9 11.3 9.9h160c3.2 0 6.2-1.2 8.3-3.3 1.8-1.7 2.9-4.2 2.9-6.7v-7.5c-0.1-20.9 5.6-41.6 16.4-59.8 10.8-18.3 26.4-33.4 45.1-43.7 37.3-20.4 82.4-20.2 119.5 0.6l6.7 3.8c2.8 1.5 6.1 1.9 9.2 1.1 2.8-0.7 5.3-2.5 6.8-5l80-135c2.7-4.5 1.1-10.2-4-13l-6.7-3.7c-18.4-10.2-33.7-25.2-44.4-43.3-33.8-57.1-13.4-130.5 45-163.5l6.6-3.7c5.1-2.9 6.6-8.5 3.9-13l-79.9-135.1c-2.2-3.4-6-5.4-10-5.3-2.1 0-4.3 0.5-6.1 1.6l-6.8 3.8c-18.3 10.5-39.1 16-60.2 16-66.5 0.2-120.6-53.5-120.8-119.9v-7.5c0-5.3-4.8-10-11.3-10l-160 0.3z m-3.4-15.5" p-id="2784"></path><path d="M512 584c39.8 0 72-32.2 72-72s-32.2-72-72-72-72 32.2-72 72 32.2 72 72 72z m0 72c-79.5 0-144-64.5-144-144s64.5-144 144-144 144 64.5 144 144-64.5 144-144 144z m0 0" p-id="2785"></path></svg>',
}

const NAMESPACE = 'BangumiCommentEnhance'

// eslint-disable-next-line unicorn/no-static-only-class
class Storage {
  static set(key, value) {
    localStorage.setItem(`${NAMESPACE}_${key}`, JSON.stringify(value))
  }

  static get(key) {
    const value = localStorage.getItem(`${NAMESPACE}_${key}`)
    return value ? JSON.parse(value) : undefined
  }

  static async init(settings) {
    const keys = Object.keys(settings)
    for (const key of keys) {
      const value = Storage.get(key)
      if (value === undefined) {
        Storage.set(key, settings[key])
      }
    }
  }
}

// create a noname header, emit a even to control the movement of whole setting dialog when dragging this header

const createNonameHeader = () => {
  const nonameHeader = document.createElement('div')
  nonameHeader.className = 'padding-row'
  nonameHeader.addEventListener('mousedown', (event) => {
    event.preventDefault()

    const container = event.target.parentElement

    // Store initial positions
    const startX = event.clientX
    const startY = event.clientY
    const startLeft = Number.parseInt(window.getComputedStyle(container).left) || 0
    const startTop = Number.parseInt(window.getComputedStyle(container).top) || 0

    // When we start dragging, remove the centering transform
    if (container.style.transform.includes('translate')) {
      const rect = container.getBoundingClientRect()
      container.style.transform = 'none'
      container.style.left = `${rect.left}px`
      container.style.top = `${rect.top}px`
    }

    const handleMouseMove = (event) => {
      // Calculate how far the mouse has moved
      const deltaX = event.clientX - startX
      const deltaY = event.clientY - startY

      // Apply that delta to the original position
      const newLeft = startLeft + deltaX
      const newTop = startTop + deltaY

      // Get container dimensions
      const containerWidth = container.offsetWidth
      const containerHeight = container.offsetHeight

      // Check if new position would be outside viewport
      if (
        newLeft < containerWidth / 2 ||
        newTop < containerHeight / 2 ||
        newLeft + containerWidth / 2 > window.innerWidth ||
        newTop + containerHeight / 2 > window.innerHeight
      ) {
        // Cancel the movement by not updating position
        return
      }

      // If we get here, the position is safe, so update it
      container.style.left = `${newLeft}px`
      container.style.top = `${newTop}px`
    }

    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', () => {
      document.removeEventListener('mousemove', handleMouseMove)
    })
  })
  return nonameHeader
}

var styles =
  '.fixed-container {\r\n  position: fixed;\r\n  z-index: 100;\r\n  width: calc(100vw - 50px);\r\n  max-width: 380px;\r\n  background-color: rgba(255, 255, 255, 0.8);\r\n  backdrop-filter: blur(8px);\r\n  left: 50%;\r\n  top: 50%;\r\n  transform: translate(-50%, -50%);\r\n  border-radius: 12px;\r\n  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);\r\n  padding: 30px;\r\n  padding-top: 0px;\r\n  text-align: center;\r\n  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;\r\n  box-sizing: border-box;\r\n  display: none;\r\n}\r\n\r\n[data-theme="dark"] .fixed-container {\r\n  background-color: rgba(30, 30, 30, 0.8);\r\n  color: #fff;\r\n}\r\n\r\n.padding-row{\r\n\twidth:100%;\r\n\theight:40px;\r\n}\r\n\r\n.dropdown-group {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  margin-bottom: 16px;\r\n}\r\n\r\n.dropdown-select {\r\n  padding: 8px;\r\n  padding-right: 16px;\r\n  border-radius: 6px;\r\n  border: 1px solid #e2e2e2;\r\n  background-color: #f5f5f5;\r\n  font-size: 14px;\r\n  width: 100%;\r\n}\r\n\r\n[data-theme="dark"] .dropdown-select {\r\n  background-color: #333;\r\n  border-color: #555;\r\n  color: #fff;\r\n}\r\n\r\n.checkbox-container {\r\n  display: flex;\r\n  align-items: center;\r\n  margin-bottom: 16px;\r\n  text-align: left;\r\n  font-size: 14px;\r\n}\r\n\r\n.checkbox-container input[type="checkbox"] {\r\n  margin-right: 12px;\r\n  transform: translateY(1.5px);\r\n}\r\n\r\n.input-group {\r\n  display: flex;\r\n  align-items: center;\r\n  margin-bottom: 16px;\r\n  justify-content: flex-start;\r\n}\r\n\r\n.input-group label {\r\n  text-align: left;\r\n  font-size: 14px;\r\n  margin-right: 8px;\r\n}\r\n\r\n.input-group input {\r\n  max-width: 40px;\r\n  padding: 6px;\r\n  border-radius: 6px;\r\n  border: 1px solid #e2e2e2;\r\n  text-align: center;\r\n}\r\n\r\n[data-theme="dark"] .input-group input {\r\n  background-color: #333;\r\n  border-color: #555;\r\n  color: #fff;\r\n}\r\n\r\n.button-group {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  gap: 12px;\r\n}\r\n\r\n.button-group button {\r\n  flex: 1;\r\n  padding: 10px;\r\n  border-radius: 6px;\r\n  border: none;\r\n  font-size: 16px;\r\n  cursor: pointer;\r\n}\r\n\r\n.cancel-btn {\r\n  background-color: white;\r\n  border: 1px solid #e2e2e2;\r\n}\r\n\r\n[data-theme="dark"] .cancel-btn {\r\n  background-color: #333;\r\n  border-color: #555;\r\n  color: #fff;\r\n}\r\n\r\n.save-btn {\r\n  background-color: #333;\r\n  color: white;\r\n}\r\n\r\n[data-theme="dark"] .save-btn {\r\n  background-color: #555;\r\n}\r\n\r\nbutton:hover {\r\n  filter: brightness(1.5);\r\n  transition: all 0.3s;\r\n}\r\n\r\nstrong svg {\r\n  max-width: 21px;\r\n  max-height: 21px;\r\n  transform: translateY(2px);\r\n  margin-right: 10px;\r\n}\r\n\r\n[data-theme="dark"] strong svg {\r\n  filter: invert(1);\r\n}\r\n\r\ninput[type="checkbox"] {\r\n  width: 20px;\r\n  height: 20px;\r\n  margin: 0;\r\n  cursor: pointer;\r\n}\r\n'

function createSettingMenu(userSettings, episodeMode = false) {
  const injectStyles = () => {
    const styleEl = document.createElement('style')
    styleEl.textContent = styles
    document.head.append(styleEl)
  }

  const createSettingsDialog = () => {
    const container = document.createElement('div')
    container.className = 'fixed-container'
    // const nonameHeader = document.createElement('div')
    // nonameHeader.className = 'padding-row'
    const nonameHeader = createNonameHeader()

    const dropdownContainer = document.createElement('div')
    dropdownContainer.className = 'dropdown-group'
    const spacerLeft = document.createElement('div')
    spacerLeft.style.width = '24px'
    const dropdown = document.createElement('select')
    dropdown.className = 'dropdown-select'

    const options = [
      { value: 'reactionCount', text: '按热度(贴贴数)排序' },
      { value: 'newFirst', text: '按时间排序(最新在前)' },
      { value: 'oldFirst', text: '按时间排序(最旧在前)' },
      { value: 'replyCount', text: '按评论数排序' },
    ]

    dropdown.append(
      ...options.map((opt) => {
        const option = document.createElement('option')
        option.value = opt.value
        option.textContent = opt.text
        return option
      }),
    )
    dropdown.value = userSettings.sortMode || 'reactionCount'
    const spacerRight = document.createElement('div')
    spacerRight.style.width = '24px'

    dropdownContainer.append($('<strong></strong>').html(Icons.sorting)[0])
    dropdownContainer.append(dropdown)
    dropdownContainer.append(spacerRight)

    // Create checkbox
    const checkboxContainers = []

    const hidePlainCommentsCheckboxContainer = new CustomCheckboxContainer(
      'hidePlainComments',
      '隐藏普通评论',
      userSettings.hidePlainComments || false,
    )

    const pinMyCommentsCheckboxContainer = new CustomCheckboxContainer(
      'showMine',
      '置顶我发表/回复我的帖子',
      userSettings.stickyMentioned || false,
    )

    const hidePrematureCommentsCheckboxContainer = new CustomCheckboxContainer(
      'hidePremature',
      '隐藏开播前发表的评论',
      userSettings.hidePremature || false,
    )

    checkboxContainers.push(
      hidePlainCommentsCheckboxContainer.getContainer(),
      pinMyCommentsCheckboxContainer.getContainer(),
    )

    if (episodeMode) {
      checkboxContainers.push(hidePrematureCommentsCheckboxContainer.getContainer())
    }

    // Create min effective number int
    const minEffGroup = document.createElement('div')
    minEffGroup.className = 'input-group'

    const minEffLabel = document.createElement('label')
    minEffLabel.htmlFor = 'minEffectiveNumber'
    minEffLabel.textContent = '最低有效字数 (>=0)'

    const minEffInput = document.createElement('input')
    minEffInput.type = 'number'
    minEffInput.id = 'minEffectiveNumber'
    minEffInput.value = userSettings.minimumFeaturedCommentLength || 0

    minEffGroup.append($('<strong></strong>').html(Icons.font)[0])
    minEffGroup.append(minEffLabel)
    minEffGroup.append(minEffInput)

    // Create max selected posts input
    const maxPostsGroup = document.createElement('div')
    maxPostsGroup.className = 'input-group'

    const maxPostsLabel = document.createElement('label')
    maxPostsLabel.htmlFor = 'maxSelectedPosts'
    maxPostsLabel.textContent = '最大精选评论数 (>0)'

    const maxPostsInput = document.createElement('input')
    maxPostsInput.type = 'number'
    maxPostsInput.id = 'maxSelectedPosts'
    maxPostsInput.value = userSettings.maxFeaturedComments || 1

    maxPostsGroup.append($('<strong></strong>').html(Icons.answerSheet)[0])
    maxPostsGroup.append(maxPostsLabel)
    maxPostsGroup.append(maxPostsInput)

    const spaceHr = document.createElement('hr')
    spaceHr.style.marginBottom = '16px'
    spaceHr.style.border = 'none'

    // Create buttons
    const buttonGroup = document.createElement('div')
    buttonGroup.className = 'button-group'

    const cancelBtn = document.createElement('button')
    cancelBtn.className = 'cancel-btn'
    cancelBtn.textContent = '取消'

    const saveBtn = document.createElement('button')
    saveBtn.className = 'save-btn'
    saveBtn.textContent = '保存'

    buttonGroup.append(cancelBtn)
    buttonGroup.append(saveBtn)

    // Assemble everything
    container.append(nonameHeader)
    container.append(dropdownContainer)
    container.append(minEffGroup)
    container.append(maxPostsGroup)
    container.append(...checkboxContainers)
    container.append(spaceHr)
    container.append(buttonGroup)

    // Add to document
    document.body.append(container)

    return {
      container,
      dropdown,
      pinMyCommentsCheckboxContainer,
      hidePlainCommentsCheckboxContainer,
      hidePrematureCommentsCheckboxContainer,
      minEffInput,
      maxPostsInput,
      cancelBtn,
      saveBtn,
    }
  }
  // Initialize settings from localStorage
  const initSettings = (elements) => {
    const {
      dropdown,
      pinMyCommentsCheckboxContainer,
      hidePlainCommentsCheckboxContainer,
      hidePrematureCommentsCheckboxContainer,
      minEffInput,
      maxPostsInput,
    } = elements

    if (localStorage.getItem('sortBy')) {
      dropdown.value = localStorage.getItem('sortBy')
    }

    if (localStorage.getItem('showMine') !== null) {
      pinMyCommentsCheckboxContainer.getInput().checked =
        localStorage.getItem('showMine') === 'true'
    }

    if (localStorage.getItem('hidePremature') !== null) {
      hidePrematureCommentsCheckboxContainer.getInput().checked =
        localStorage.getItem('hidePremature') === 'true'
    }

    if (localStorage.getItem('hidePlainComments') !== null) {
      hidePlainCommentsCheckboxContainer.getInput().checked =
        localStorage.getItem('hidePlainComments') === 'true'
    }

    if (localStorage.getItem('minEffectiveNumber')) {
      minEffInput.value = localStorage.getItem('minEffectiveNumber')
    }

    if (localStorage.getItem('maxSelectedPosts')) {
      maxPostsInput.value = localStorage.getItem('maxSelectedPosts')
    }
  }

  // Save settings
  const saveSettings = (elements) => {
    const {
      container,
      dropdown,
      pinMyCommentsCheckboxContainer,
      hidePrematureCommentsCheckboxContainer,
      hidePlainCommentsCheckboxContainer,
      minEffInput,
      maxPostsInput,
    } = elements

    Storage.set(
      'minimumFeaturedCommentLength',
      Math.max(Number.parseInt(minEffInput.value) || 0, 0),
    )
    Storage.set(
      'maxFeaturedComments',
      Number.parseInt(maxPostsInput.value) > 0 ? Number.parseInt(maxPostsInput.value) : 1,
    )

    Storage.set('hidePlainComments', hidePlainCommentsCheckboxContainer.getInput().checked)
    Storage.set('stickyMentioned', pinMyCommentsCheckboxContainer.getInput().checked)
    Storage.set('sortMode', dropdown.value)
    Storage.set('stickyMentioned', pinMyCommentsCheckboxContainer.getInput().checked)

    if (episodeMode) {
      Storage.set('hidePremature', hidePrematureCommentsCheckboxContainer.getInput().checked)
    }

    // Trigger custom event
    const event = new CustomEvent('settingsSaved')
    document.dispatchEvent(event)

    // jQuery compatibility
    if (window.jQuery) {
      jQuery(document).trigger('settingsSaved')
    }

    hideDialog(container)
  }

  // Show dialog
  const showDialog = (container) => {
    container.style.display = 'block'
  }

  // Hide dialog
  const hideDialog = (container) => {
    container.style.display = 'none'
  }

  // Main initialization function
  const init = () => {
    // Inject the styles
    injectStyles()
    // Create the dialog
    const elements = createSettingsDialog()
    // Initialize settings
    initSettings(elements)

    // Setup event listeners
    elements.saveBtn.addEventListener('click', () => saveSettings(elements))
    elements.cancelBtn.addEventListener('click', () => hideDialog(elements.container))

    // // Add window resize handler to center the dialog when window is resized
    // window.addEventListener('resize', () => {
    //   if (elements.container.style.display === 'block') {
    //     elements.container.style.left = '50%'
    //     elements.container.style.top = '50%'
    //     elements.container.style.transform = 'translate(-50%, -50%)'
    //   }
    // })

    // Expose API
    window.BCE.settingsDialog = {
      show: () => showDialog(elements.container),
      hide: () => hideDialog(elements.container),
      save: () => saveSettings(elements),
      getElements: () => elements,
    }
  }

  // Auto-initialize when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init)
  } else {
    init()
  }
}

const BGM_EP_REGEX = /^https:\/\/(((fast\.)?bgm\.tv)|(chii\.in)|(bangumi\.tv))\/ep\/\d+/
const BGM_GROUP_REGEX =
  /^https:\/\/(((fast\.)?bgm\.tv)|(chii\.in)|(bangumi\.tv))\/group\/topic\/\d+/

// quickSort is not strictly needed cause JavaScript has built-in sort method based on quicksort/selection algorithm
function quickSort(arr, sortKey, changeCompareDirection = false) {
  if (arr.length <= 1) {
    return arr
  }
  const pivot = arr[0]
  const left = []
  const right = []
  for (let i = 1; i < arr.length; i++) {
    const element = arr[i]
    const elementImportant = element.important || false
    const pivotImportant = pivot.important || false
    let compareResult

    if (elementImportant !== pivotImportant) {
      compareResult = elementImportant // true if element is important and pivot is not
    } else if (changeCompareDirection) {
      compareResult = element[sortKey] < pivot[sortKey]
    } else {
      compareResult = element[sortKey] > pivot[sortKey]
    }

    if (compareResult) {
      left.push(element)
    } else {
      right.push(element)
    }
  }
  return quickSort(left, sortKey, changeCompareDirection).concat(
    pivot,
    quickSort(right, sortKey, changeCompareDirection),
  )
}
function purifiedDatetimeInMillionSeconds(timestamp) {
  return new Date(timestamp.trim().replace('- ', '')).getTime()
}

function processComments(userSettings) {
  // check if the target element is valid
  const username = $('.idBadgerNeue .avatar').attr('href')
    ? $('.idBadgerNeue .avatar').attr('href').split('/user/')[1]
    : ''
  const preservedPostID =
    $(location).attr('href').split('#').length > 1 ? $(location).attr('href').split('#')[1] : null
  const allCommentRows = $('.row.row_reply.clearit')
  let plainCommentsCount = 0
  const featuredCommentsCount = 0
  let prematureCommentsCount = 0
  const minimumContentLength = userSettings.minimumFeaturedCommentLength
  const container = $('#comment_list')
  const plainCommentElements = []
  const featuredCommentElements = []
  const lastRow = allCommentRows.last()
  let preservedRow = null
  let isLastRowFeatured = false

  // Get first broadcast time for episode pages
  let firstBroadcastDate = null
  if (BGM_EP_REGEX.test(location.href) && userSettings.hidePremature) {
    try {
      const broadcastTimeMatch = document
        .querySelectorAll('.tip')[0]
        .innerHTML.match(/\d{4}-\d{1,2}-\d{1,2}/)
      if (broadcastTimeMatch && broadcastTimeMatch[0]) {
        const dateParts = broadcastTimeMatch[0].split('-')
        firstBroadcastDate = new Date(
          Number.parseInt(dateParts[0]),
          Number.parseInt(dateParts[1]) - 1, // Month is 0-indexed in JS
          Number.parseInt(dateParts[2]),
        )
        firstBroadcastDate.setHours(0, 0, 0, 0) // Set to beginning of the day
      }
    } catch (error) {
      console.error('Error parsing broadcast date:', error)
    }
  }

  allCommentRows.each(function (index, row) {
    const that = $(this)
    const content = $(row)
      .find(BGM_EP_REGEX.test(location.href) ? '.message.clearit' : '.inner')
      .text()

    // Check if comment is before broadcast date
    let isBeforeBroadcast = false
    if (firstBroadcastDate && BGM_EP_REGEX.test(location.href) && userSettings.hidePremature) {
      try {
        const postTimeMatch = that
          .find('.re_info')
          .text()
          .match(/\d{4}-\d{1,2}-\d{1,2}/)
        if (postTimeMatch && postTimeMatch[0]) {
          const postDateParts = postTimeMatch[0].split('-')
          const postDate = new Date(
            Number.parseInt(postDateParts[0]),
            Number.parseInt(postDateParts[1]) - 1,
            Number.parseInt(postDateParts[2]),
          )
          postDate.setHours(0, 0, 0, 0)

          if (postDate < firstBroadcastDate) {
            isBeforeBroadcast = true
            prematureCommentsCount++
          }
        }
      } catch (error) {
        console.error('Error parsing post date:', error)
      }
    }

    let commentScore = 0
    // prioritize @me comments on
    const highlightMentionedColor = '#ff8c00'
    const subReplyContent = that.find('.topic_sub_reply')
    const commentsCount = subReplyContent.find('.sub_reply_bg').length
    const mentionedInMainComment =
      userSettings.stickyMentioned &&
      that.find('.avatar').attr('href').split('/user/')[1] === username
    let mentionedInSubReply = false
    if (mentionedInMainComment) {
      that.css('border-color', highlightMentionedColor)
      that.css('border-width', '1px')
      that.css('border-style', 'dashed')
      commentScore += 10000
    }
    that.find(`.topic_sub_reply .sub_reply_bg.clearit`).each(function (index, element) {
      if (userSettings.stickyMentioned && $(element).attr('data-item-user') === username) {
        $(element).css('border-color', highlightMentionedColor)
        $(element).css('border-width', '1px')
        $(element).css('border-style', 'dashed')
        commentScore += 1000
        mentionedInSubReply = true
      }
    })
    const important = mentionedInMainComment || mentionedInSubReply
    that.find('span.num').each(function (index, element) {
      commentScore += Number.parseInt($(element).text())
    })
    const hasPreservedReply = preservedPostID && that.find(`#${preservedPostID}`).length > 0
    if (hasPreservedReply) preservedRow = row
    if (!hasPreservedReply) subReplyContent.hide()
    const timestampArea = that.find('.action').first()
    if (commentsCount !== 0) {
      const a = $(
        `<a class="expand_all" href="javascript:void(0)" style="margin:0 3px 0 5px;"><span class="ico ico_reply">展开(+${commentsCount})</span></a>`,
      )
      mentionedInSubReply && a.css('color', highlightMentionedColor)
      a.on('click', function () {
        subReplyContent.slideToggle()
      })
      const el = $(`<div class="action"></div>`).append(a)
      timestampArea.after(el)
    }
    // check if this comment meets the requirement of minimumContentLength
    const isShortReply = content.trim().length < minimumContentLength
    let isFeatured =
      userSettings.sortMode === 'reactionCount' ? commentScore >= 1 : commentsCount >= 1
    if (isShortReply || featuredCommentsCount >= userSettings.maxFeaturedComments) {
      isFeatured = false
    }
    // conserved reply must be fixed
    if (hasPreservedReply || important) {
      isFeatured = true
    }

    const timestamp = isFeatured
      ? $(row)
          .find('.action:eq(0) small')
          .first()
          .contents()
          .filter(function () {
            return this.nodeType === 3 // Node.TEXT_NODE === 3
          })
          .first()
          .text()
      : $(row).find('small').text().trim()

    if (isBeforeBroadcast && userSettings.hidePremature) {
      $(row).addClass('premature-comment').hide()
    }

    if (isFeatured) {
      // check if current row is the last row by comparing the id
      if (row.id === lastRow[0].id) {
        isLastRowFeatured = true
      }
      featuredCommentElements.push({
        element: row,
        score: commentScore,
        commentsCount,
        timestampNumber: purifiedDatetimeInMillionSeconds(timestamp),
        important,
      })
    } else {
      plainCommentsCount++
      plainCommentElements.push({
        element: row,
        score: commentScore,
        timestamp,
        timestampNumber: purifiedDatetimeInMillionSeconds(timestamp),
      })
    }
  })

  return {
    plainCommentsCount,
    featuredCommentsCount,
    prematureCommentsCount,
    container,
    plainCommentElements,
    featuredCommentElements,
    preservedRow,
    lastRow,
    isLastRowFeatured,
  }
}

;(async function () {
  if (!BGM_EP_REGEX.test(location.href) && !BGM_GROUP_REGEX.test(location.href)) {
    return
  }
  Storage.init({
    hidePlainComments: true,
    minimumFeaturedCommentLength: 15,
    maxFeaturedComments: 99,
    sortMode: 'reactionCount',
    stickyMentioned: false,
    hidePremature: false,
  })
  window.BCE = window.BCE || {}

  const userSettings = {
    hidePlainComments: Storage.get('hidePlainComments'),
    minimumFeaturedCommentLength: Storage.get('minimumFeaturedCommentLength'),
    maxFeaturedComments: Storage.get('maxFeaturedComments'),
    sortMode: Storage.get('sortMode'),
    stickyMentioned: Storage.get('stickyMentioned'),
    hidePremature: Storage.get('hidePremature'),
  }
  const sortModeData = userSettings.sortMode || 'reactionCount'
  /**
   * Main
   */
  let {
    plainCommentsCount,
    container,
    plainCommentElements,
    featuredCommentElements,
    preservedRow,
    lastRow,
    isLastRowFeatured,
  } = processComments(userSettings)
  let stateBar = container.find('.row_state.clearit')
  if (stateBar.length === 0) {
    stateBar = $(`<div id class="row_state clearit"></div>`)
  }
  // Create toggle button with appropriate text based on current state
  const toggleButtonText = userSettings.hidePlainComments
    ? `点击展开剩余${plainCommentsCount}条普通评论`
    : `点击折叠${plainCommentsCount}条普通评论`

  const toggleHiddenCommentsInfoText = () => {
    const curText = $(hiddenCommentsInfo).text()
    if (curText.includes('展开')) {
      hiddenCommentsInfo.text(`点击折叠${plainCommentsCount}条普通评论`)
    } else {
      hiddenCommentsInfo.text(`点击展开剩余${plainCommentsCount}条普通评论`)
    }
  }

  const hiddenCommentsInfo = $(
    `<div class="filtered" id="toggleFilteredBtn" style="cursor:pointer;color:#48a2c3;">${toggleButtonText}</div>`,
  ).click(function () {
    const commentList = $('#comment_list_plain')
    commentList.slideToggle()
    toggleHiddenCommentsInfoText()
  })

  stateBar.append(hiddenCommentsInfo)
  container.find('.row').detach()
  const menuBarCSSProperties = {
    display: 'inline-block',
    width: '20px',
    height: '20px',
    transform: 'translate(0, -3px)',
    margin: '0 0 0 5px',
    cursor: 'pointer',
  }
  const settingBtn = $('<strong></strong>')
    .css(menuBarCSSProperties)
    .html(Icons.gear)
    .click(() => window.BCE.settingsDialog.show())

  const jumpToNewestBtn = $('<strong></strong>')
    .css(menuBarCSSProperties)
    .html(Icons.newest)
    .click(() => {
      $('#comment_list_plain').slideDown()
      hiddenCommentsInfo.text(`点击折叠${plainCommentsCount}条普通评论`)
      // get the target element with the same id as lastRow inside the FeatureElements
      const targetId = lastRow[0].id
      const targetItem = isLastRowFeatured
        ? featuredCommentElements.find((item) => item.element.id === targetId)
        : plainCommentElements.at(-1)
      $('html, body').animate({
        scrollTop: $(targetItem.element).offset().top,
      })
      $(lastRow).css({
        'background-color': '#ffd966',
        transition: 'background-color 0.5s ease-in-out',
      })
      setTimeout(() => {
        $(lastRow).css('background-color', '')
      }, 750)
    })

  const menuBar = $(
    '<h3 style="padding:10px;display:flex;width:100%;align-items:center;">所有精选评论</h3>',
  )
    .append(settingBtn)
    .append(jumpToNewestBtn)
  if (BGM_EP_REGEX.test(location.href)) {
    const showPrematureBtn = $('<strong></strong>')
      .css(menuBarCSSProperties)
      .html(Icons.eyeOpen)
      .click(() => {
        $('.premature-comment').toggle()
      })
    menuBar.append(showPrematureBtn)
  }
  container.append(menuBar)
  const trinity = {
    reactionCount() {
      featuredCommentElements = quickSort(featuredCommentElements, 'reactionCount', false)
    },
    replyCount() {
      featuredCommentElements = quickSort(featuredCommentElements, 'replyCount', false)
    },
    oldFirst() {
      featuredCommentElements = quickSort(featuredCommentElements, 'timestampNumber', true)
    },
    newFirst() {
      featuredCommentElements = quickSort(featuredCommentElements, 'timestampNumber', false)
    },
  }
  trinity[sortModeData]()
  /**
   * Append components
   */
  featuredCommentElements.forEach(function (element) {
    container.append($(element.element))
  })
  plainCommentElements.forEach(function (element) {
    container.append($(element.element))
  })
  container.append(stateBar)
  // Create container for plain comments
  const plainCommentsContainer = $('<div id="comment_list_plain" style="margin-top:2rem;"></div>')

  // Only hide plain comments if the setting is enabled
  if (userSettings.hidePlainComments) {
    plainCommentsContainer.hide()
  }

  // Add plain comments to the container
  plainCommentElements.forEach(function (element) {
    plainCommentsContainer.append($(element.element))
  })

  container.append(plainCommentsContainer)
  // Scroll to conserved row if exists
  if (preservedRow) {
    $('html, body').animate({
      scrollTop: $(preservedRow).offset().top,
    })
  }
  $('#sortMethodSelect').val(sortModeData)
  // Auto-expand plain comments if there are few featured comments and plain comments are hidden
  if (userSettings.hidePlainComments === true) {
    $('#toggleFilteredBtn').click()
  }
  createSettingMenu(userSettings, BGM_EP_REGEX.test(location.href))
  // control center
  $(document).on('settingsSaved', () => {
    location.reload()
  })
})()