Greasy Fork

Greasy Fork is available in English.

NGA Smiles Manager

NGA表情管理器

当前为 2022-08-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        NGA Smiles Manager
// @namespace   http://greasyfork.icu/users/263018
// @version     1.0.0
// @author      snyssss
// @description NGA表情管理器

// @match       *://bbs.nga.cn/*
// @match       *://ngabbs.com/*
// @match       *://nga.178.com/*

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_addValueChangeListener

// @noframes
// ==/UserScript==

((ui, poster) => {
  if (!ui) return;
  if (!poster) return;

  // 数据操作
  const manager = (() => {
    const KEY = `NGA_SMILES_MANAGER`;

    const data = {};

    const fetchData = (pid) =>
      new Promise((resolve, reject) => {
        const api = `/read.php?pid=${pid}`;

        fetch(api)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = async () => {
              const parser = new DOMParser();

              const doc = parser.parseFromString(reader.result, "text/html");

              const verify = doc.querySelector("#m_posts");

              if (verify) {
                const subject = doc.querySelector("#postsubject0").innerHTML;

                const content = doc.querySelector("#postcontent0").innerHTML;

                const items = content.match(/(?<=\[img\])(.+?)(?=\[\/img\])/g);

                if (items.length) {
                  resolve({
                    name: subject,
                    items,
                  });
                } else {
                  reject("图楼内容有误");
                }
              } else {
                reject(doc.title);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject("服务器异常");
          });
      });

    const assign = (next) => {
      Object.getOwnPropertyNames(data).forEach((property) => {
        delete data[property];
      });

      Object.getOwnPropertyNames(next).forEach((property) => {
        data[property] = next[property];
      });
    };

    const save = () => {
      GM_setValue(KEY, data);
    };

    const load = () => {
      assign(
        GM_getValue(KEY) || {
          [0]: {
            syncInterval: 3600,
          },
        }
      );
    };

    const settings = () => {
      if (Object.keys(data).length < 1) {
        load();
      }

      return data[0];
    };

    const updateSettings = (values) => {
      const entity = settings();

      Object.getOwnPropertyNames(values).forEach((property) => {
        entity[property] = values[property];
      });

      save();
    };

    const list = () => {
      if (Object.keys(data).length < 1) {
        load();
      }

      return Object.keys(data)
        .filter((key) => key > 0)
        .reduce((root, key) => {
          return [...root, data[key]];
        }, []);
    };

    const get = (pid) => {
      return list().find((item) => item.pid === pid);
    };

    const set = (pid, values) => {
      const entity = get(pid);

      if (entity) {
        Object.getOwnPropertyNames(values).forEach((property) => {
          entity[property] = values[property];
        });
      } else {
        const index = Math.max(...Object.keys(data), 0) + 1;

        data[index] = {
          pid,
          name: `#${pid}`,
          error: "",
          enabled: true,
          syncDate: null,
          ...values,
        };
      }

      save();
    };

    const sync = async (pid) => {
      const { syncInterval } = settings();

      const syncDate = new Date().getTime();

      const entity = get(pid);

      if (
        syncInterval > 0 &&
        entity &&
        entity.syncDate + syncInterval * 1000 > syncDate
      ) {
        return false;
      }

      try {
        const { name, items } = await fetchData(pid);

        set(pid, {
          name: name || `#${pid}`,
          error: "",
          syncDate,
        });

        GM_setValue(pid, items);
      } catch (error) {
        set(pid, {
          error,
          syncDate,
        });

        return false;
      }

      return true;
    };

    const add = async (url) => {
      const params = new URLSearchParams(url.substring(url.indexOf("?")));

      const pid = params.get("pid");

      if (pid === null) {
        alert("图楼地址有误");
        return false;
      }

      return await sync(pid);
    };

    const remove = (pid) => {
      GM_deleteValue(pid);

      Object.keys(data).forEach((key) => {
        if (data[key].pid === pid) {
          delete data[key];
        }
      });

      save();
    };

    GM_addValueChangeListener(KEY, function (_, prev, next) {
      assign(next);
    });

    return {
      add,
      set,
      sync,
      remove,
      list,
      settings,
      updateSettings,
    };
  })();

  // STYLE
  GM_addStyle(`
    .s-user-info-container:not(:hover) .ah {
      display: none !important;
    }
    .s-table-wrapper {
      height: calc((2em + 10px) * 11 + 3px);
      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
  const u = (() => {
    const modules = {};

    const tabContainer = (() => {
      const c = document.createElement("div");

      c.className = "w100";
      c.innerHTML = `
          <div class="right_" style="margin-bottom: 5px;">
              <table class="stdbtn" cellspacing="0">
                  <tbody>
                      <tr></tr>
                  </tbody>
              </table>
          </div>
          <div class="clear"></div>
          `;

      return c;
    })();

    const tabPanelContainer = (() => {
      const c = document.createElement("div");

      c.style = "width: 80vw;";

      return c;
    })();

    const content = (() => {
      const c = document.createElement("div");

      c.append(tabContainer);
      c.append(tabPanelContainer);

      return c;
    })();

    const addModule = (() => {
      const tc = tabContainer.getElementsByTagName("tr")[0];
      const cc = tabPanelContainer;

      return (module) => {
        const tabBox = document.createElement("td");

        tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;

        const tab = tabBox.childNodes[0];

        const toggle = () => {
          Object.values(modules).forEach((item) => {
            if (item.tab === tab) {
              item.tab.className = "nobr";
              item.content.style = "display: block";
              item.refresh();
            } else {
              item.tab.className = "nobr silver";
              item.content.style = "display: none";
            }
          });
        };

        tc.append(tabBox);
        cc.append(module.content);

        tab.onclick = toggle;

        modules[module.name] = {
          ...module,
          tab,
          toggle,
        };

        return modules[module.name];
      };
    })();

    return {
      content,
      modules,
      addModule,
    };
  })();

  // 列表
  (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <div class="s-table-wrapper">
          <table class="s-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">标题</th>
                <th class="c2">异常信息</th>
                <th class="c3" width="1">同步时间</th>
                <th class="c4" width="1">是否启用</th>
                <th class="c5" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
      `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(manager.list()).forEach((item) => {
          const { pid, name, error, enabled, syncDate } = item;

          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
            <td class="c1">
                <a href="/read.php?pid=${pid}" class="b nobr">${name}</a>
            </td>
            <td class="c2">
                <span class="nobr">${error}</span>
            </td>
            <td class="c3">
                <span class="nobr">${new Date(syncDate).toLocaleString()}</span>
            </td>
            <td class="c4">
                <div style="text-align: center;">
                    <input type="checkbox" ${
                      enabled ? `checked="checked"` : ""
                    } />
                </div>
            </td>
            <td class="c5">
                <div class="s-button-group">
                    <button>同步</button>
                    <button>删除</button>
                </div>
            </td>
            `;

          const enabledElement = tc.querySelector(`INPUT[type="checkbox"]`);

          const save = () => {
            manager.set(pid, {
              enabled: enabledElement.checked ? 1 : 0,
            });
          };

          enabledElement.onchange = save;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = async () => {
            await manager.sync(pid);

            refresh();
          };

          actions[1].onclick = () => {
            if (confirm("是否确认?")) {
              manager.remove(pid);

              refresh();
            }
          };

          container.appendChild(tc);
        });

        {
          const tc = document.createElement("tr");

          tc.className = `row${
            (container.querySelectorAll("TR").length % 2) + 1
          }`;

          tc.innerHTML = `
              <td class="c1" colspan="4">
                  <div class="s-input-wrapper">
                    <input value="" />
                  </div>
              </td>
              <td class="c5">
                  <div class="s-button-group">
                    <button>添加</button>
                  </div>
              </td>
            `;

          const inputElement = tc.querySelector("INPUT");
          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = async () => {
            if (inputElement.value) {
              if (await manager.add(inputElement.value)) {
                refresh();
              }
            }
          };

          container.appendChild(tc);
        }
      };

      return func;
    })();

    u.addModule({
      name: "列表",
      content,
      refresh,
    });
  })();

  // 通用设置
  (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";

      return c;
    })();

    const refresh = (() => {
      const container = content;

      const func = () => {
        container.innerHTML = "";

        const { syncInterval } = manager.settings();

        // 自动同步
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <div>自动同步设置</div>
            <div></div>
          `;

          [
            {
              label: "1小时",
              value: 3600,
            },
            {
              label: "1天",
              value: 3600 * 24,
            },
            {
              label: "从不",
              value: 0,
            },
          ].forEach(({ label, value }, index) => {
            const ele = document.createElement("SPAN");

            ele.innerHTML += `
            <input id="s-st-${index}" type="radio" name="syncInterval" ${
              syncInterval === value && "checked"
            }>
            <label for="s-st-${index}" style="cursor: pointer;">${label}</label>
            `;

            const items = ele.querySelector("input");

            items.onchange = () => {
              if (items.checked) {
                manager.updateSettings({
                  syncInterval: value,
                });
              }
            };

            tc.querySelectorAll("div")[1].append(ele);
          });

          container.appendChild(tc);
        }
      };

      return func;
    })();

    u.addModule({
      name: "设置",
      content,
      refresh,
    });
  })();

  // 增加菜单项
  (() => {
    const title = "表情管理";

    let window;

    ui.mainMenu.addItemOnTheFly(title, null, () => {
      if (window === undefined) {
        window = ui.createCommmonWindow();
      }

      u.modules["列表"].toggle();

      window._.addContent(null);
      window._.addTitle(title);
      window._.addContent(u.content);
      window._.show();
    });
  })();

  // 加载表情
  const loadSmiles = (loaded) => {
    if (loaded) return;

    const { correctAttachUrl } = ui;

    const tabs = poster.selectSmilesw._.__c.firstElementChild;
    const contents = poster.selectSmilesw._.__c.lastElementChild;

    manager.list().forEach((item) => {
      const { pid, name, enabled } = item;

      if (enabled) {
        const tab = document.createElement("BUTTON");
        const content = document.createElement("DIV");

        tab.className = "block_txt_big";
        tab.innerText = name;
        tab.onclick = async () => {
          contents.childNodes.forEach((c) => {
            if (c !== content) {
              c.style.display = "none";
            } else {
              c.style.display = "";
            }
          });

          if (content.childNodes.length === 0) {
            await manager.sync(pid);

            const list = GM_getValue(pid) || [];

            list.forEach((s) => {
              const smile = document.createElement("IMG");

              smile.src = s.indexOf("http") < 0 ? correctAttachUrl(s) : s;
              smile.style = "max-height: 200px";
              smile.onclick = () => {
                poster.selectSmilesw._.hide();
                poster.addText(`[img]${s}[/img]`);
              };

              content.appendChild(smile);
            });
          }
        };

        tabs.appendChild(tab);
        contents.appendChild(content);
      }
    });
  };

  // 加载脚本
  (() => {
    const hookFunction = (object, functionName, callback) => {
      ((originalFunction) => {
        object[functionName] = function () {
          const returnValue = originalFunction.apply(this, arguments);

          callback.apply(this, [returnValue, originalFunction, arguments]);

          return returnValue;
        };
      })(object[functionName]);
    };

    hookFunction(poster, "selectSmiles", (returnValue) =>
      loadSmiles(returnValue)
    );
  })();
})(commonui, postfunc);