Greasy Fork

Greasy Fork is available in English.

NGA Smiles Manager

NGA表情管理器

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

// ==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);