标签 Greasy Fork 下的文章

各位早中下午好,不知道各位看视频爱不爱看弹幕,反正我是离不开了,恨不得去电影院都能开着弹幕看。就好这一口氛围感。从去年开始 B 站就回到以前的大航海时代,资源满天飞,但还得和版权方躲猫猫,导致要么是 N 个 UP 上传各种神命名的番剧,要么就是来不及看被补档了,导致弹幕分布在各个角落,为了自己看的爽一点,搞了个弹幕合并器脚本。完成得差不多了,分享出来。

预览

搜索界面

通过关键词或 BV 号快速查找目标视频。
搜索界面

管理界面

选择分 P 并设置时间偏移。
管理界面

弹幕效果

多源弹幕同屏显示,增强互动感。
弹幕效果

支持时间跳转

适配时间戳弹幕,支持点击跳转。
支持时间跳转

安装说明

  1. 确保浏览器已安装 Tampermonkey 插件。
  2. 前往 Greasy Fork 点击安装(推荐)。
  3. 或者下载并手动安装本仓库Github中的 Bilibili_Danmaku_Merger.js 脚本。
  4. 刷新 B 站视频页面即可在播放器下方或侧边看到“合并弹幕”按钮。

周五了,利用摸鱼时间为 2Libra 打造了新的油猴脚本 - 2Libra Plus。

✨ 主要功能

1. 通知中心增强

  • 未读消息高亮:自动检测通知列表中的未读条目,并在左侧添加醒目的橙色竖线标记,帮助你快速定位未处理的通知。
  • 自动已读(可选):支持进入通知页后自动将当前页消息标记为已读,减少重复点击操作(默认开启,可在设置中关闭)。

screencapture

2. 主题列表增强

  • 回复时间颜色渐变:根据你上次在首页查看时间,将最新回复显示得更醒目,较久之前的回复颜色更浅,帮助你一眼区分「最近更新」和「很久没动」的帖子。为了避免频繁刷新带来的视觉抖动,「上次查看时间」在 5 分钟内不会更新;最新回复会使用 --color-primary 颜色展示,更加醒目。

screencapture

3. 个性化设置

  • 提供可视化的设置面板,可随时开启或关闭特定功能,按需定制你的使用体验。

!. 还会有更多功能...

⬇️ 安装方法

相关链接

建议反馈

大家如有需要的功能,尽管提,觉得有用我会考虑加进去。

  • 使用 Cmd/Ctrl + Shift + K 快捷键一键开启 / 关闭搜索模式
  • 自动输入 /search 激活搜索模式
  • 再次按下快捷键即可退出搜索模式

📌 转载信息
原作者:
0x2F4D2
转载时间:
2026/1/4 12:26:08

但好在 2k 还是免费
只能搞个自动切换 2k 了

代码
// ==UserScript==
// @name         Genspark Nano Banana Auto 2K
// @namespace    https://genspark.ai/
// @version      0.1.0
// @description  Auto-select 2K image size on https://www.genspark.ai/agents?type=moa_generate_image
// @match        https://www.genspark.ai/agents*
// @match        https://genspark.ai/agents*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const TARGET_TYPE = 'moa_generate_image';
  const TARGET_SIZE_LABEL = '2K';

  const DEBUG = false;
  const LOG_PREFIX = '[Genspark Auto 2K]';
  const log = (...args) => {
    if (!DEBUG) return;
    // eslint-disable-next-line no-console
    console.log(LOG_PREFIX, ...args);
  };

  const normalizeText = (text) => (text || '').replace(/\s+/g, ' ').trim();

  const isTargetPage = () => {
    try {
      const url = new URL(location.href);
      return url.pathname === '/agents' && url.searchParams.get('type') === TARGET_TYPE;
    } catch {
      return false;
    }
  };

  const safeClick = (el) => {
    if (!el) return false;
    const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
    const x = rect ? Math.floor(rect.left + rect.width / 2) : 0;
    const y = rect ? Math.floor(rect.top + rect.height / 2) : 0;

    if (typeof PointerEvent === 'function' && rect && rect.width > 0 && rect.height > 0) {
      el.dispatchEvent(
        new PointerEvent('pointerdown', {
          bubbles: true,
          cancelable: true,
          view: window,
          pointerType: 'mouse',
          isPrimary: true,
          clientX: x,
          clientY: y,
          button: 0,
          buttons: 1,
        })
      );
      el.dispatchEvent(
        new PointerEvent('pointerup', {
          bubbles: true,
          cancelable: true,
          view: window,
          pointerType: 'mouse',
          isPrimary: true,
          clientX: x,
          clientY: y,
          button: 0,
          buttons: 0,
        })
      );
    }

    el.dispatchEvent(
      new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window,
        clientX: x,
        clientY: y,
        button: 0,
      })
    );

    return true;
  };

  const waitFor = (predicate, { timeoutMs = 15_000, pollMs = 250 } = {}) =>
    new Promise((resolve, reject) => {
      const start = Date.now();
      let done = false;

      const cleanup = () => {
        obs.disconnect();
        clearInterval(poller);
        clearTimeout(timeout);
      };

      const tryResolve = () => {
        if (done) return;

        let value = null;
        try {
          value = predicate();
        } catch {
          value = null;
        }

        if (value) {
          done = true;
          cleanup();
          resolve(value);
        }
      };

      const obs = new MutationObserver(tryResolve);
      const poller = setInterval(tryResolve, pollMs);
      const timeout = setTimeout(() => {
        if (done) return;
        done = true;
        cleanup();
        reject(new Error(`Timeout after ${timeoutMs}ms`));
      }, timeoutMs);

      obs.observe(document.documentElement, { childList: true, subtree: true });
      tryResolve();
    });

  const findSettingsPanel = () => {
    const panels = Array.from(document.querySelectorAll('.settings-panel'));
    return (
      panels.find((p) => normalizeText(p.textContent).includes('Image Size')) ||
      panels.find((p) => normalizeText(p.textContent).includes('Aspect Ratio')) ||
      null
    );
  };

  const findSettingTrigger = () => {
    const textNodes = Array.from(document.querySelectorAll('.text'));
    const settingText = textNodes.find((el) => normalizeText(el.textContent) === 'Setting');
    if (!settingText) return null;
    return (
      settingText.closest('[role="button"],button,a,.models-selected,.model-selected') || settingText
    );
  };

  const findSizeOption = (label) => {
    const panel = findSettingsPanel();
    if (!panel) return null;
    const options = Array.from(panel.querySelectorAll('.size-option'));
    const normalizedLabel = normalizeText(label);
    return (
      options.find((el) => normalizeText(el.textContent) === normalizedLabel) ||
      options.find((el) => normalizeText(el.textContent).startsWith(normalizedLabel)) ||
      null
    );
  };

  const ensure2KOnce = async () => {
    if (!isTargetPage()) return false;

    const trigger = await waitFor(findSettingTrigger).catch(() => null);
    if (!trigger) return false;

    const alreadyOpen = !!findSettingsPanel();
    if (!alreadyOpen) safeClick(trigger);

    const panel = await waitFor(findSettingsPanel).catch(() => null);
    if (!panel) return false;

    const option2k = findSizeOption(TARGET_SIZE_LABEL);
    if (!option2k) return false;

    if (!option2k.classList.contains('selected')) {
      log('Selecting', TARGET_SIZE_LABEL);
      safeClick(option2k);
    } else {
      log('Already selected', TARGET_SIZE_LABEL);
    }
    return true;
  };

  let runInFlight = null;
  const scheduleEnsure2K = () => {
    if (runInFlight) return;
    runInFlight = (async () => {
      try {
        await ensure2KOnce();
      } finally {
        runInFlight = null;
      }
    })();
  };

  const hookHistory = () => {
    const emit = () => window.dispatchEvent(new Event('genspark:auto-2k:urlchange'));
    const wrap = (method) =>
      function (...args) {
        const ret = method.apply(this, args);
        emit();
        return ret;
      };

    history.pushState = wrap(history.pushState);
    history.replaceState = wrap(history.replaceState);
    window.addEventListener('popstate', emit, { passive: true });
    window.addEventListener('genspark:auto-2k:urlchange', scheduleEnsure2K, { passive: true });
  };

  hookHistory();
  scheduleEnsure2K();
})();

📌 转载信息
原作者:
lueluelue
转载时间:
2025/12/30 12:10:07