- // ==UserScript==
- // @name NGA Account Switcher
- // @name:zh-CN NGA 账号切换
- // @namespace https://gf.qytechs.cn/users/263018
- // @version 1.0.0
- // @author snyssss
- // @description 快速切换多个账号
- // @license MIT
-
- // @match *://bbs.nga.cn/*
- // @match *://ngabbs.com/*
- // @match *://nga.178.com/*
-
- // @require https://update.gf.qytechs.cn/scripts/486070/1377381/NGA%20Library.js
-
- // @grant GM_addStyle
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_registerMenuCommand
- // @grant unsafeWindow
-
- // @run-at document-start
- // ==/UserScript==
-
- (() => {
- // 声明泥潭主模块
- let commonui;
-
- // 声明 UI
- let ui;
-
- // STYLE
- GM_addStyle(`
- .s-table-wrapper {
- max-height: 80vh;
- overflow-y: auto;
- }
- .s-table {
- margin: 0;
- }
- .s-table th,
- .s-table td {
- position: relative;
- white-space: nowrap;
- }
- .s-table th {
- position: sticky;
- top: 2px;
- z-index: 1;
- }
- .s-table input:not([type]), .s-table input[type="text"] {
- margin: 0;
- box-sizing: border-box;
- height: 100%;
- width: 100%;
- }
- .s-input-wrapper {
- position: absolute;
- top: 6px;
- right: 6px;
- bottom: 6px;
- left: 6px;
- }
- .s-text-ellipsis {
- display: flex;
- }
- .s-text-ellipsis > * {
- flex: 1;
- width: 1px;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .s-button-group {
- margin: -.1em -.2em;
- }
- `);
-
- /**
- * UI
- */
- class UI {
- /**
- * 标签
- */
- static label = "账号切换";
-
- /**
- * 弹出窗
- */
- window;
-
- /**
- * 视图元素
- */
- views = {};
-
- /**
- * 初始化
- */
- constructor() {
- this.init();
- }
-
- /**
- * 初始化,创建基础视图,初始化通用设置
- */
- init() {
- const tabs = this.createTabs({
- className: "right_",
- });
-
- const content = this.createElement("DIV", [], {
- style: "width: 400px;",
- });
-
- const container = this.createElement("DIV", [tabs, content]);
-
- this.views = {
- tabs,
- content,
- container,
- };
- }
-
- /**
- * 创建元素
- * @param {String} tagName 标签
- * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML
- * @param {*} properties 额外属性
- * @returns {HTMLElement} 元素
- */
- createElement(tagName, content, properties = {}) {
- const element = document.createElement(tagName);
-
- // 写入内容
- if (typeof content === "string") {
- element.innerHTML = content;
- } else {
- if (Array.isArray(content) === false) {
- content = [content];
- }
-
- content.forEach((item) => {
- if (item === null) {
- return;
- }
-
- if (typeof item === "string") {
- element.append(item);
- return;
- }
-
- element.appendChild(item);
- });
- }
-
- // 对 A 标签的额外处理
- if (tagName.toUpperCase() === "A") {
- if (Object.hasOwn(properties, "href") === false) {
- properties.href = "javascript: void(0);";
- }
- }
-
- // 附加属性
- Object.entries(properties).forEach(([key, value]) => {
- element[key] = value;
- });
-
- return element;
- }
-
- /**
- * 创建按钮
- * @param {String} text 文字
- * @param {Function} onclick 点击事件
- * @param {*} properties 额外属性
- */
- createButton(text, onclick, properties = {}) {
- return this.createElement("BUTTON", text, {
- ...properties,
- onclick,
- });
- }
-
- /**
- * 创建按钮组
- * @param {Array} buttons 按钮集合
- */
- createButtonGroup(...buttons) {
- return this.createElement("DIV", buttons, {
- className: "s-button-group",
- });
- }
-
- /**
- * 创建表格
- * @param {Array} headers 表头集合
- * @param {*} properties 额外属性
- * @returns {HTMLElement} 元素和相关函数
- */
- createTable(headers, properties = {}) {
- const rows = [];
-
- const ths = headers.map((item, index) =>
- this.createElement("TH", item.label, {
- ...item,
- className: `c${index + 1}`,
- })
- );
-
- const tr =
- ths.length > 0
- ? this.createElement("TR", ths, {
- className: "block_txt_c0",
- })
- : null;
-
- const thead = tr !== null ? this.createElement("THEAD", tr) : null;
-
- const tbody = this.createElement("TBODY", []);
-
- const table = this.createElement("TABLE", [thead, tbody], {
- ...properties,
- className: "s-table forumbox",
- });
-
- const wrapper = this.createElement("DIV", table, {
- className: "s-table-wrapper",
- });
-
- const intersectionObserver = new IntersectionObserver((entries) => {
- if (entries[0].intersectionRatio <= 0) return;
-
- const list = rows.splice(0, 10);
-
- if (list.length === 0) {
- return;
- }
-
- intersectionObserver.disconnect();
-
- tbody.append(...list);
-
- intersectionObserver.observe(tbody.lastElementChild);
- });
-
- const add = (...columns) => {
- const tds = columns.map((column, index) => {
- if (ths[index]) {
- const { center, ellipsis } = ths[index];
-
- const properties = {};
-
- if (center) {
- properties.style = "text-align: center;";
- }
-
- if (ellipsis) {
- properties.className = "s-text-ellipsis";
- }
-
- column = this.createElement("DIV", column, properties);
- }
-
- return this.createElement("TD", column, {
- className: `c${index + 1}`,
- });
- });
-
- const tr = this.createElement("TR", tds, {
- className: `row${(rows.length % 2) + 1}`,
- });
-
- intersectionObserver.disconnect();
-
- rows.push(tr);
-
- intersectionObserver.observe(tbody.lastElementChild || tbody);
- };
-
- const update = (e, ...columns) => {
- const row = e.target.closest("TR");
-
- if (row) {
- const tds = row.querySelectorAll("TD");
-
- columns.map((column, index) => {
- if (ths[index]) {
- const { center, ellipsis } = ths[index];
-
- const properties = {};
-
- if (center) {
- properties.style = "text-align: center;";
- }
-
- if (ellipsis) {
- properties.className = "s-text-ellipsis";
- }
-
- column = this.createElement("DIV", column, properties);
- }
-
- if (tds[index]) {
- tds[index].innerHTML = "";
- tds[index].append(column);
- }
- });
- }
- };
-
- const remove = (e) => {
- const row = e.target.closest("TR");
-
- if (row) {
- tbody.removeChild(row);
- }
- };
-
- const clear = () => {
- rows.splice(0);
- intersectionObserver.disconnect();
-
- tbody.innerHTML = "";
- };
-
- Object.assign(wrapper, {
- add,
- update,
- remove,
- clear,
- });
-
- return wrapper;
- }
-
- /**
- * 创建标签组
- * @param {*} properties 额外属性
- */
- createTabs(properties = {}) {
- const tabs = this.createElement(
- "DIV",
- `<table class="stdbtn" cellspacing="0">
- <tbody>
- <tr></tr>
- </tbody>
- </table>`,
- properties
- );
-
- return this.createElement(
- "DIV",
- [
- tabs,
- this.createElement("DIV", [], {
- className: "clear",
- }),
- ],
- {
- style: "display: none; margin-bottom: 5px;",
- }
- );
- }
-
- /**
- * 创建标签
- * @param {Element} tabs 标签组
- * @param {String} label 标签名称
- * @param {Number} order 标签顺序,重复则跳过
- * @param {*} properties 额外属性
- */
- createTab(tabs, label, order, properties = {}) {
- const group = tabs.querySelector("TR");
-
- const items = [...group.childNodes];
-
- if (items.find((item) => item.order === order)) {
- return;
- }
-
- if (items.length > 0) {
- tabs.style.removeProperty("display");
- }
-
- const tab = this.createElement("A", label, {
- ...properties,
- className: "nobr silver",
- onclick: () => {
- if (tab.className === "nobr") {
- return;
- }
-
- group.querySelectorAll("A").forEach((item) => {
- if (item === tab) {
- item.className = "nobr";
- } else {
- item.className = "nobr silver";
- }
- });
-
- if (properties.onclick) {
- properties.onclick();
- }
- },
- });
-
- const wrapper = this.createElement("TD", tab, {
- order,
- });
-
- const anchor = items.find((item) => item.order > order);
-
- group.insertBefore(wrapper, anchor || null);
-
- return wrapper;
- }
-
- /**
- * 创建对话框
- * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出
- * @param {String} title 对话框的标题
- * @param {HTMLElement} content 对话框的内容
- */
- createDialog(anchor, title, content) {
- let window;
-
- const show = () => {
- if (window === undefined) {
- window = commonui.createCommmonWindow();
- }
-
- window._.addContent(null);
- window._.addTitle(title);
- window._.addContent(content);
- window._.show();
- };
-
- if (anchor) {
- anchor.onclick = show;
- } else {
- show();
- }
-
- return window;
- }
-
- /**
- * 弹窗确认
- * @param {String} message 提示信息
- * @returns {Promise}
- */
- confirm(message = "是否确认?") {
- return new Promise((resolve, reject) => {
- const result = confirm(message);
-
- if (result) {
- resolve();
- return;
- }
-
- reject();
- });
- }
-
- /**
- * 渲染视图
- */
- renderView() {
- // 创建或打开弹出窗
- if (this.window === undefined) {
- this.window = this.createDialog(
- this.views.anchor,
- this.constructor.label,
- this.views.container
- );
- } else {
- this.window._.show();
- }
-
- // 启用第一个模块
- this.views.tabs.querySelector("A").click();
- }
-
- /**
- * 渲染
- */
- render() {
- this.renderView();
- }
- }
-
- /**
- * 基础模块
- */
- class Module {
- /**
- * 模块名称
- */
- static name;
-
- /**
- * 模块标签
- */
- static label;
-
- /**
- * 顺序
- */
- static order;
-
- /**
- * UI
- */
- ui;
-
- /**
- * 视图元素
- */
- views = {};
-
- /**
- * 初始化并绑定UI,注册(不可用) UI
- * @param {UI} ui UI
- */
- constructor(ui) {
- this.ui = ui;
-
- this.init();
- }
-
- /**
- * 获取列表
- */
- get list() {
- return GM_getValue(this.constructor.name, {});
- }
-
- /**
- * 写入列表
- */
- set list(value) {
- GM_setValue(this.constructor.name, value);
- }
-
- /**
- * 初始化,创建基础视图和组件
- */
- init() {
- if (this.views.container) {
- this.destroy();
- }
-
- const { ui } = this;
-
- const container = ui.createElement("DIV", []);
-
- this.views = {
- container,
- };
-
- this.initComponents();
- }
-
- /**
- * 初始化组件
- */
- initComponents() {}
-
- /**
- * 销毁
- */
- destroy() {
- Object.values(this.views).forEach((view) => {
- if (view.parentNode) {
- view.parentNode.removeChild(view);
- }
- });
-
- this.views = {};
- }
-
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- container.innerHTML = "";
- container.appendChild(this.views.container);
- }
- }
-
- /**
- * 账号列表
- */
- class AccountList extends Module {
- /**
- * 模块名称
- */
- static name = "data";
-
- /**
- * 模块标签
- */
- static label = "账号";
-
- /**
- * 顺序
- */
- static order = 10;
-
- /**
- * 表格列
- * @returns {Array} 表格列集合
- */
- columns() {
- return [
- { label: "昵称" },
- { label: "登录(不可用)时间" },
- { label: "操作", width: 1 },
- ];
- }
-
- /**
- * 表格项
- * @param {Object} item 账号信息
- * @returns {Array} 表格项集合
- */
- column(item) {
- const { ui } = this;
-
- const { table } = this.views;
-
- const { uid, username, timestamp } = item;
-
- // 昵称
- const name = (() => {
- const label = username ? "@" + username : "#" + uid;
-
- return ui.createElement("A", `[${label}]`, {
- className: "b nobr",
- href: `/nuke.php?func=ucp&uid=${uid}`,
- });
- })();
-
- // 登录(不可用)时间
- const time = ui.createElement(
- "SPAN",
- commonui.time2dis(timestamp / 1000),
- {
- className: "nobr",
- }
- );
-
- // 操作
- const buttons = (() => {
- const toggle = ui.createButton("切换", (e) => {
- loadData(uid).catch((err) => {
- alert(err.message);
-
- removeData(uid);
-
- table.remove(e);
- });
- });
-
- const remove = ui.createButton("删除", (e) => {
- ui.confirm().then(() => {
- removeData(uid);
-
- table.remove(e);
- });
- });
-
- if (unsafeWindow.__CURRENT_UID === uid) {
- return ui.createButtonGroup(remove);
- }
-
- return ui.createButtonGroup(toggle, remove);
- })();
-
- return [name, time, buttons];
- }
-
- /**
- * 初始化组件
- */
- initComponents() {
- super.initComponents();
-
- const { tabs, content } = this.ui.views;
-
- const table = this.ui.createTable(this.columns());
-
- const tab = this.ui.createTab(
- tabs,
- this.constructor.label,
- this.constructor.order,
- {
- onclick: () => {
- this.render(content);
- },
- }
- );
-
- Object.assign(this.views, {
- tab,
- table,
- });
-
- this.views.container.appendChild(table);
- }
-
- /**
- * 渲染
- * @param {HTMLElement} container 容器
- */
- render(container) {
- super.render(container);
-
- const { list } = this;
-
- const { table } = this.views;
-
- if (table) {
- const { add, clear } = table;
-
- clear();
-
- Object.values(list).forEach((item) => {
- const column = this.column(item);
-
- add(...column);
- });
- }
- }
- }
-
- /**
- * 渲染 UI
- */
- const renderUI = () => {
- if (commonui && commonui.mainMenuItems) {
- if (ui === undefined) {
- ui = new UI();
-
- new AccountList(ui);
- }
-
- ui.render();
- }
- };
-
- /**
- * 处理 commonui 模块
- * @param {*} value commonui
- */
- const handleCommonui = (value) => {
- // 绑定主模块
- commonui = value;
-
- // 拦截 mainMenu 模块,处理 init 事件
- Tools.interceptProperty(commonui, "mainMenu", {
- afterSet: (mainMenu) => {
- // 加入菜单
- if (mainMenu && mainMenu.addItemOnTheFly) {
- mainMenu.addItemOnTheFly(`账号切换`, null, renderUI);
- }
- },
- });
- };
-
- /**
- * 注册(不可用)脚本菜单
- */
- const registerMenu = () => {
- GM_registerMenuCommand(`账号切换`, renderUI);
- };
-
- /**
- * 拦截登录(不可用)页面
- */
- const handleLogin = () => {
- if (unsafeWindow.document.title === "账号操作") {
- // 处理 __API 模块
- const handleLoginAPI = (value) => {
- if (value) {
- // 拦截 get 方法,从中取得登录(不可用)成功后的信息
- Tools.interceptProperty(value, "get", {
- beforeGet: (...args) => {
- if (args[0] === "loginSuccess") {
- const { uid, username, token } = JSON.parse(args[1]);
-
- saveData(uid, username, token);
- }
-
- return args;
- },
- });
- }
- };
-
- if (unsafeWindow.__API) {
- handleLoginAPI(unsafeWindow.__API);
- return;
- }
-
- Tools.interceptProperty(unsafeWindow, "__API", {
- afterSet: (value) => {
- handleLoginAPI(value);
- },
- });
- }
- };
-
- /**
- * 载入数据
- * @param {String} uid 用户 ID
- */
- const loadData = async (uid) => {
- const list = GM_getValue(AccountList.name, {});
- const item = list[uid];
-
- if (item) {
- const { cid } = item;
-
- const url = `/nuke.php?__lib=login&__act=login_set_cookie_quick`;
-
- const form = new FormData();
-
- form.append("uid", uid);
- form.append("cid", cid);
-
- const response = await fetch(url, {
- method: "POST",
- body: form,
- });
-
- const result = await Tools.readForumData(response, false);
-
- const parser = new DOMParser();
-
- const doc = parser.parseFromString(result, "text/html");
-
- const message = doc.body.innerText.replace(/\s/g, "");
-
- if (message === "SUCCESS") {
- unsafeWindow.location.reload();
- return;
- }
- }
-
- throw new Error("登录(不可用)状态失效,请重新登录(不可用)");
- };
-
- /**
- * 保存数据
- */
- const saveData = (uid, username, cid) => {
- const list = GM_getValue(AccountList.name, {});
-
- list[uid] = {
- uid,
- username,
- cid,
- timestamp: new Date().getTime(),
- };
-
- GM_setValue(AccountList.name, list);
- };
-
- /**
- * 删除数据
- * @param {String} uid 用户 ID
- */
- const removeData = (uid) => {
- const list = GM_getValue(AccountList.name, {});
-
- delete list[uid];
-
- GM_setValue(AccountList.name, list);
- };
-
- // 主函数
- (async () => {
- // 注册(不可用)脚本菜单
- registerMenu();
-
- // 拦截登录(不可用)页面
- handleLogin();
-
- // 处理 commonui 模块
- if (unsafeWindow.commonui) {
- handleCommonui(unsafeWindow.commonui);
- return;
- }
-
- Tools.interceptProperty(unsafeWindow, "commonui", {
- afterSet: (value) => {
- handleCommonui(value);
- },
- });
- })();
- })();