// ==UserScript==
// @name AT&CF Problems Note
// @namespace https://github.com/yxz2333
// @version 1.1.0
// @description 在 kenkoooo.com 和 cf.kira924age.com 表格网站添加题目笔记
// @author Lynia
// @match *://kenkoooo.com/atcoder/*
// @match *://cf.kira924age.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js
// @resource bootstrapCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
// @license MIT
// ==/UserScript==
// 导入 bootstrapCSS
GM_addStyle(GM_getResourceText("bootstrapCSS"))
const BODY = document.querySelector('body')
/**
* @namespace Lynia
* @description 主命名空间
*/
const Lynia = {}
/**
* @namespace state
* @description 状态信息
*/
Lynia.state = {
/** @type {boolean} 当前 url 是否为 AtCoder Problem */
isAtCoder: window.location.href.includes('kenkoooo.com/atcoder'),
/** @type {boolean} 当前 url 是否为 Codeforces Problem */
isCodeforces: window.location.href.includes('cf.kira924age.com/#/table/')
}
/**
* @namespace element
* @description 要全局用到的元素
*/
Lynia.element = {
/** @type {HTMLElement} Body */
body: undefined,
/** @description 弹出框 */
dialog: new class {
/** @type {HTMLDivElement} 弹出框本身 */
modal = undefined
/** @type {HTMLDivElement} 装文字的元素 */
text = undefined
/** @type {HTMLDivElement} 包裹题目难度圆和题目链接的 div */
problemCell = undefined
/** @type {HTMLAnchorElement} 题目链接元素 */
problemLink = undefined
/** @type {Boolean} 是否在笔记编辑状态 */
isEditing = false
// ------------------------------
// 一些有的没的子节点
/** @type {HTMLDivElement} Dialog */
#modalDialog = undefined
/** @type {HTMLDivElement} Content */
#modalContent = undefined
/** @type {HTMLDivElement} Header */
#modalHeader = undefined
/** @type {HTMLDivElement} Body */
#modalBody = undefined
/** @type {HTMLDivElement} Footer */
#modalFooter = undefined
/** @type {HTMLElement} 位于 Header 的关闭按钮 */
#close1 = undefined
/** @type {HTMLElement} 位于 Footer 的关闭按钮 */
#close2 = undefined
/** @type {HTMLElement} 位于 Footer 的编辑按钮 */
#edit = undefined
/** @type {HTMLElement} 位于 Body 的文字输入框的 div */
#textareaDiv = undefined
/** @type {HTMLTextAreaElement} 文字输入框 textareaDiv 内的实际输入框*/
#textarea = undefined
/** @type {HTMLLabelElement} 文字输入框 textareaDiv 内的标签 */
#label = undefined
// ------------------------------
/** @method 退出编辑状态 */
endEditing() {
this.isEditing = false
if (this.#modalBody.contains(this.#textareaDiv)) this.#modalBody.removeChild(this.#textareaDiv)
if (!this.#modalBody.contains(this.text)) this.#modalBody.appendChild(this.text)
this.#edit.textContent = "编辑"
this.#close2.textContent = "关闭"
this.#close2.setAttribute("data-bs-dismiss", "modal")
this.#close2.removeEventListener("click", this.endEditing)
// 读取笔记
const localValue = GM_getValue(`${this.problemLink.href}`, null)
if (localValue) {
this.text.innerHTML = localValue
this.problemCell.style.setProperty("background-color", "rgba(236, 240, 5, 0.7)", "important")
}
else {
this.text.innerHTML = "本题尚无笔记"
this.problemCell.style.removeProperty("background-color")
}
}
init() {
// modal 主体结构
this.modal = document.createElement('div')
this.#modalDialog = document.createElement("div")
this.#modalContent = document.createElement('div')
this.#modalHeader = document.createElement('div')
this.#modalBody = document.createElement('div')
this.#modalFooter = document.createElement('div')
Lynia.element.body.appendChild(this.modal) // 挂载到 body 下
this.modal.appendChild(this.#modalDialog)
this.#modalDialog.appendChild(this.#modalContent)
this.#modalContent.appendChild(this.#modalHeader)
this.#modalContent.appendChild(this.#modalBody)
this.#modalContent.appendChild(this.#modalFooter)
this.modal.id = "dialog"
this.modal.classList.add("modal", "fade")
this.#modalDialog.classList.add("modal-dialog", "modal-dialog-centered", "modal-dialog-scrollable", "modal-xl")
this.#modalContent.classList.add('modal-content')
this.#modalHeader.classList.add('modal-header')
this.#modalBody.classList.add('modal-body')
this.#modalFooter.classList.add('modal-footer')
// problemLink 题目链接
this.problemLink = document.createElement('a')
this.problemLink.id = "problemLink"
this.problemLink.style.fontSize = "26px"
this.problemLink.style.fontWeight = "bold"
// Header 的关闭按钮
this.#close1 = document.createElement('button')
this.#close1.type = "button"
this.#close1.classList.add("btn-close")
this.#close1.setAttribute("data-bs-dismiss", "modal")
this.#close1.setAttribute("aria-label", "Close")
// text 文本
this.text = document.createElement('div')
this.text.id = "text"
// Footer 的关闭按钮
this.#close2 = document.createElement('button')
this.#close2.type = "button"
this.#close2.textContent = "关闭"
this.#close2.classList.add("btn", "btn-secondary")
this.#close2.setAttribute("data-bs-dismiss", "modal")
// Footer 的编辑按钮
this.#edit = document.createElement('button')
this.#edit.type = "button"
this.#edit.textContent = "编辑"
this.#edit.classList.add("btn", "btn-primary")
// textarea 文字输入框 内包裹 textarea 和 label
this.#textareaDiv = document.createElement("div")
this.#textareaDiv.classList.add("form-floating")
// textarea
this.#textarea = document.createElement("textarea")
this.#textarea.classList.add("form-control")
this.#textarea.style.height = "500px"
this.#textarea.id = "textarea"
this.#textareaDiv.appendChild(this.#textarea)
// label 标签
this.#label = document.createElement("label")
this.#label.setAttribute("for", "textarea")
this.#label.textContent = "笔记"
this.#textareaDiv.appendChild(this.#label)
// 创建的子节点绑定到对应 modal 结构
this.#modalHeader.appendChild(this.problemLink)
this.#modalHeader.appendChild(this.#close1)
this.#modalBody.appendChild(this.text)
this.#modalFooter.appendChild(this.#close2)
this.#modalFooter.appendChild(this.#edit)
// edit 绑定点击事件
this.#edit.addEventListener("click", () => {
this.isEditing = !this.isEditing
if (this.isEditing) {
// 进入编辑模式
// 读取笔记
const localValue = GM_getValue(`${this.problemLink.href}`, null)
if (localValue) this.#textarea.value = Lynia.toolMethod.InnerHTMLToString(localValue)
else this.#textarea.value = ''
this.#modalBody.removeChild(this.text)
this.#modalBody.appendChild(this.#textareaDiv)
this.#edit.textContent = "保存"
this.#close2.textContent = "取消"
this.#close2.removeAttribute("data-bs-dismiss")
this.#close2.addEventListener("click", this.endEditing.bind(this))
} else {
// 退出编辑模式
// 保存当前笔记
GM_setValue(`${this.problemLink.href}`, Lynia.toolMethod.StringToInnerHTML(this.#textarea.value))
this.endEditing.bind(this)()
}
})
}
},
init() {
this.body = document.querySelector('body')
this.dialog.init()
},
}
/**
* @namespace observer
* @description 监听器
*/
Lynia.observer = {
tab: {
/**
* @type {MutationObserver}
* @description 表格种类切换选项监听器,监听当前 table 种类,tab 更新时重新遍历新的表格元素
*/
observer: undefined,
init() {
try {
let targetElement = null
if (Lynia.state.isAtCoder) targetElement = document.querySelector(".table-tab")
else if (Lynia.state.isCodeforces) targetElement = document.querySelector("#radio-buttons")
else throw new Error("url错误")
if (targetElement === null) throw new Error("找不到表格选项")
this.observer = new MutationObserver(() => {
handleTableProblems()
})
this.observer.observe(targetElement, {
attributes: true,
attributeFilter: ['class'],
subtree: true
})
} catch (error) {
console.log(error.message)
}
}
},
page: {
/**
* @type {MutationObserver}
* @description 表格页码切换监听器,监听当前 table 页码,tab 更新时重新遍历新的表格元素
*/
observer: undefined,
init() {
try {
let targetElement = null
if (Lynia.state.isAtCoder) return
else if (Lynia.state.isCodeforces) targetElement = document.querySelector(".ant-table-pagination")
else throw new Error("url错误")
if (targetElement === null) throw new Error("找不到表格选项")
this.observer = new MutationObserver(() => {
handleTableProblems()
})
this.observer.observe(targetElement, {
attributes: true,
attributeFilter: ['class'],
subtree: true
})
} catch (error) {
console.log(error.message)
}
}
},
init() {
this.tab.init()
this.page.init()
}
}
/**
* @namespace toolMethod
* @description 工具函数
*/
Lynia.toolMethod = {
/** @method 正常字符串转InnerHTML @param {String} text */
StringToInnerHTML(text) { return text.replaceAll("\n", '<br>') },
/** @method InnerHTML转正常字符串 @param {String} text */
InnerHTMLToString(text) { return text.replaceAll('<br>', "\n") }
}
/** @method handleTableProblems 处理表格里的问题单元格 */
const handleTableProblems = () => {
// 找出所有表格元素
let tableProblemElements
if (Lynia.state.isAtCoder) tableProblemElements = document.querySelectorAll(".table-problem")
else if (Lynia.state.isCodeforces) tableProblemElements = document.querySelectorAll(".cell-element")
else return
tableProblemElements.forEach(
(element) => {
if (!(element instanceof HTMLElement)) return
// 找到表格里的 难度圆 和 题目链接
let circle, link
if (Lynia.state.isAtCoder) circle = element.querySelector('.difficulty-circle')
else circle = element.querySelector('.common-difficulty-circle')
link = element.querySelector('a')
if (circle && link) {
circle.setAttribute("data-bs-toggle", "modal")
circle.setAttribute("data-bs-target", "#dialog")
circle.addEventListener("click", () => {
// 更新当前 click 的 cell
Lynia.element.dialog.problemCell = element
// 更新当前 click 的 cell 的链接元素
for (let attr of link.attributes)
Lynia.element.dialog.problemLink.setAttribute(attr.name, attr.value)
Lynia.element.dialog.problemLink.textContent = link.textContent
// dialog 退出 edit 状态
Lynia.element.dialog.endEditing()
})
// 如果本题存在笔记,表格染色
const localValue = GM_getValue(`${link.href}`, null)
if (localValue) element.style.setProperty("background-color", "rgba(236, 240, 5, 0.7)", "important")
else element.style.removeProperty("background-color")
}
}
)
}
/** @method main 主函数 */
function main() {
handleTableProblems()
}
// ------------------------------
// 脚本加载入口
setTimeout(() => {
Lynia.element.init()
Lynia.observer.init()
main()
}, 2000)
// ------------------------------