嘉立创EDA专业版增强脚本

对PC端嘉立创EDA专业版进行触摸适配,以及显示FPS等功能增强

// ==UserScript==
// @name         嘉立创EDA专业版增强脚本
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  对PC端嘉立创EDA专业版进行触摸适配,以及显示FPS等功能增强
// @author       github@xiaowine
// @match        https://pro.lceda.cn/editor*
// @require      https://cdn.jsdelivr.net/gh/hammerjs/hammer.js@ff687ea0daa3c806b9accd2ecb1a46165ea3c00a/hammer.min.js
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.registerMenuCommand
// @run-at       document-end
// @license      GPL
// @noframes
// ==/UserScript==

// 配置管理类
class ConfigManager {
  static DEFAULT_CONFIG = {
    enablePan: false,
    enablePinch: false,
    enablePress: false,
    showFPS: false,
    showCoupon: false,
    lastUpdateTime: '',  // 添加最后更新时间
    couponData: null     // 添加优惠券数据
  };

  constructor () {
    this.config = null;
    this.listeners = new Set();
    this.menuCommands = new Map(); // 存储菜单命令的引用
    this.menuItems = [
      { key: 'enablePan', text: '启用触摸拖动' },
      { key: 'enablePinch', text: '启用触摸缩放' },
      { key: 'enablePress', text: '启用触摸长按' },
      { key: 'showFPS', text: '显示FPS' },
      { key: 'showCoupon', text: '获取商城优惠' }
    ];
  }

  // 加载配置
  async load () {
    try {
      const savedConfig = await GM.getValue('touchConfig', null);
      this.config = savedConfig ? JSON.parse(savedConfig) : ConfigManager.DEFAULT_CONFIG;
    } catch (error) {
      console.error('加载配置失败:', error);
      this.config = ConfigManager.DEFAULT_CONFIG;
    }
    return this.config;
  }

  // 保存配置
  async save () {
    await GM.setValue('touchConfig', JSON.stringify(this.config));
    this.notifyListeners();
  }

  // 切换配置项
  async toggleSetting (key) {
    if (this._updating) return; // 防止重复触发
    this.config[key] = !this.config[key];
    await this.save();
    await this.updateMenu(key);
  }

  // 获取配置
  getConfig () {
    return this.config;
  }

  // 注册配置变更监听器
  addChangeListener (listener) {
    this.listeners.add(listener);
  }

  // 移除配置变更监听器
  removeChangeListener (listener) {
    this.listeners.delete(listener);
  }

  // 通知所有监听器
  notifyListeners () {
    this.listeners.forEach(listener => listener(this.config));
  }

  // 初始化菜单
  async initMenu () {
    // 确保先清理旧菜单
    await this.unregisterAllMenus();

    for (const { key, text } of this.menuItems) {
      const command = await GM.registerMenuCommand(
        `${this.config[key] ? '✅' : '❌'} ${text}`,
        () => this.toggleSetting(key)
      );
      this.menuCommands.set(key, command);
    }
  }

  // 更新菜单项显示
  async updateMenu (key) {
    // 防止重复更新
    if (this._updating) return;
    this._updating = true;

    try {
      const item = this.menuItems.find(item => item.key === key);
      if (item) {
        // 先注销所有旧的菜单命令
        await this.unregisterAllMenus();

        // 重新注册所有菜单命令
        await this.initMenu();
      }
    } finally {
      this._updating = false;
    }
  }

  // 注销所有菜单命令
  async unregisterAllMenus () {
    for (const command of this.menuCommands.values()) {
      if (command) {
        await GM.unregisterMenuCommand(command);
      }
    }
    this.menuCommands.clear();
  }
}

// TouchEventHandler类修改
class TouchEventHandler {
  constructor (targetElement, configManager) {
    this.targetElement = targetElement;
    this.hammer = null;
    this.scaleThreshold = 0.1;
    this.scrollSensitivity = 0.1;
    this.lastScale = 1;
    this.configManager = configManager;

    // 监听配置变更
    this.configManager.addChangeListener(() => {
      this.destroy();
      this.init();
    });
  }

  init () {
    if (!this.targetElement) {
      console.log("未找到目标元素");
      return false;
    }

    console.log("目标元素已获取,初始化Hammer.js:", this.targetElement);

    // 修改Hammer初始化方式
    this.hammer = new Hammer.Manager(this.targetElement, {
      touchAction: 'none',
      inputClass: Hammer.TouchInput
    });

    // 添加识别器
    this.hammer.add(new Hammer.Pan({ direction: Hammer.DIRECTION_ALL }));
    this.hammer.add(new Hammer.Pinch());
    this.hammer.add(new Hammer.Press({ time: 500 }));

    this.addEventListeners();
    return true;
  }

  getSVGCenter () {
    const rect = this.targetElement.getBoundingClientRect();
    return {
      x: rect.left + rect.width / 2,
      y: rect.top + rect.height / 2,
    };
  }
  addEventListeners () {
    console.log("添加Hammer.js事件监听");
    const config = this.configManager.getConfig();

    if (config.enablePan) {
      this.hammer.on('panstart', (ev) => {
        console.log('Pan start triggered:', ev);
        const mouseDownEvent = new MouseEvent("mousedown", {
          bubbles: true,
          cancelable: true,
          button: 1,
          clientX: ev.center.x,
          clientY: ev.center.y
        });
        this.targetElement.dispatchEvent(mouseDownEvent);
      });

      this.hammer.on('panmove', (ev) => {
        console.log('Pan move:', { x: ev.center.x, y: ev.center.y });
        const mouseMoveEvent = new MouseEvent("mousemove", {
          bubbles: true,
          cancelable: true,
          button: 1,
          clientX: ev.center.x,
          clientY: ev.center.y,
        });
        this.targetElement.dispatchEvent(mouseMoveEvent);
      });
    }

    if (config.enablePinch) {
      this.hammer.on('pinch', (ev) => {
        console.log('Pinch:', { scale: ev.scale, lastScale: this.lastScale });
        const scaleDiff = ev.scale - this.lastScale;
        if (Math.abs(scaleDiff) > this.scaleThreshold) {
          const center = this.getSVGCenter();
          console.log('Pinch threshold reached:', { scaleDiff, center });
          const scrollEvent = new WheelEvent("wheel", {
            bubbles: true,
            cancelable: true,
            deltaY: scaleDiff > 0 ? -10 : 10,
            clientX: center.x,
            clientY: center.y,
          });
          this.targetElement.dispatchEvent(scrollEvent);
          this.lastScale = ev.scale;
        }
      });
    }

    if (config.enablePress) {
      this.hammer.on('press', (ev) => {
        const rightClickEvent = new MouseEvent("contextmenu", {
          bubbles: true,
          cancelable: true,
          button: 2,
          clientX: ev.center.x,
          clientY: ev.center.y,
        });
        this.targetElement.parentNode.dispatchEvent(rightClickEvent);
      });
    }
  }

  destroy () {
    if (this.hammer) {
      console.log("正在移除Hammer.js事件监听");
      this.hammer.destroy();
      this.hammer = null;
    }
    console.log("Hammer.js事件监听已移除");
  }
}

// UI增强处理类
class UIEnhancer {
  constructor (configManager) {
    this.configManager = configManager;
    this.fpsCounter = {
      element: null,
      frameCount: 0,
      previousTimestamp: 0,
      animationFrameHandle: null,
      currentFps: 0,
      interval: 1000,
      originalText: '' // 保存原始用户名
    };

    this.configManager.addChangeListener(() => {
      if (!this.configManager.getConfig().showFPS) {
        console.log('UIEnhancer: Config changed, updating UI');
        this.startFPSCounter();
      } else {
        this.stopFPSCounter();
      }
    });
  }

  init () {
    this.findElements();
    if (!this.configManager.getConfig().showFPS) return;
    console.log('UIEnhancer: Starting initialization');
    this.startFPSCounter();
  }

  findElements () {
    console.log('UIEnhancer: Finding username element');
    // 查找用户名span元素
    const usernameSpan = document.querySelector('#loginUsername span');
    if (usernameSpan) {
      console.log('UIEnhancer: Username element found');
      this.fpsCounter.element = usernameSpan;
      this.fpsCounter.originalText = usernameSpan.textContent;
    } else {
      console.log('UIEnhancer: Username element not found');
    }
  }

  updateFps () {
    const currentTimestamp = Date.now();
    this.fpsCounter.frameCount++;

    if (currentTimestamp > this.fpsCounter.interval + this.fpsCounter.previousTimestamp) {
      this.fpsCounter.currentFps = Math.round(
        (this.fpsCounter.frameCount * 1000) / (currentTimestamp - this.fpsCounter.previousTimestamp)
      );
      this.fpsCounter.frameCount = 0;
      this.fpsCounter.previousTimestamp = currentTimestamp;

      if (this.fpsCounter.element) {
        console.log('UIEnhancer: FPS updated to', this.fpsCounter.currentFps);
        this.fpsCounter.element.textContent = `FPS: ${this.fpsCounter.currentFps}`;
      }
    }

    this.fpsCounter.animationFrameHandle = requestAnimationFrame(() => this.updateFps());
  }

  startFPSCounter () {
    console.log('UIEnhancer: Starting FPS counter');
    if (!this.fpsCounter.animationFrameHandle && this.fpsCounter.element) {
      this.fpsCounter.frameCount = 0;
      this.fpsCounter.previousTimestamp = Date.now();
      this.updateFps();
    }
  }

  stopFPSCounter () {
    console.log('UIEnhancer: Stopping FPS counter');
    if (this.fpsCounter.animationFrameHandle) {
      cancelAnimationFrame(this.fpsCounter.animationFrameHandle);
      this.fpsCounter.animationFrameHandle = null;
      this.fpsCounter.currentFps = 0;
      if (this.fpsCounter.element) {
        this.fpsCounter.element.textContent = this.fpsCounter.originalText;
      }
    }
  }

  destroy () {
    if (!this.configManager.getConfig().showFPS) return;
    console.log('UIEnhancer: Destroying');
    this.stopFPSCounter();
  }
}

class DiscountHandler {
  constructor (configManager) {
    this.configManager = configManager;
    this.runTimeUrl = 'https://szlcsc-help.xiaowine.cc/run_time.txt';
    this.couponUrl = 'https://szlcsc-help.xiaowine.cc/simple_coupon_details.json';

    this.configManager.addChangeListener(() => {
      if (!this.configManager.getConfig().showCoupon) return
      console.log('DiscountHandler: Config changed');
      this.checkForUpdates();
      this.updateCouponDialog();
      this.listener();
    });
  }

  async init () {
    if (!this.configManager.getConfig().showCoupon) return
    console.log('DiscountHandler: Starting initialization');
    await this.checkForUpdates();

    // Watch for dialog creation 
    if (this.observer) {
      this.observer.disconnect();
    }

    this.observer = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
          if (node.id === 'chooseDeviceDialogContainer_dialog_box') {
            this.updateCouponDialog();
            this.listener();
            this.observer.disconnect();
            this.observer = null;
          }
        });
      });
    });

    this.observer.observe(document.body, {
      childList: true,
      subtree: true
    });
    setInterval(() => {
      this.checkForUpdates();
    }, 60000);

  }
  addCouponData (shopItems) {
    try {
      Array.from(shopItems).forEach(item => {
        const observer = new MutationObserver((_mutations, obs) => {
          let category = null
          let brandHrefId = null
          const categoryElement = item.querySelector('.two-tit_6gtEt a:nth-child(2)');
          if (categoryElement) {
            category = categoryElement?.getAttribute('title');
          }

          const brandElement = item.querySelector('.l02_zb_ms1h6 .brand-name');
          if (brandElement) {
            const brandHref = brandElement?.getAttribute('href');
            brandHrefId = brandHref?.match(/\/brand\/(\d+)\.html/)?.[1] || null;
          }

          if (category || brandHrefId) {
            obs.disconnect();
          }
          if (category && brandHrefId) {
            const couponData = this.configManager.getConfig().couponData;
            if (couponData && couponData[brandHrefId]) {
              const couponContainer = item.querySelector('.l02_yb_3cN-b');
              if (couponContainer) {
                const couponListItem = document.createElement('li');
                const couponLink = document.createElement('a');
                couponLink.href = `https://list.szlcsc.com/brand/${brandHrefId}.html`;
                couponLink.target = '_blank';
                couponLink.innerHTML = couponData[brandHrefId]["coupon_name"];
                couponLink.style.color = 'red';
                couponLink.style.fontSize = '12px';
                couponListItem.appendChild(couponLink);
                couponContainer.appendChild(couponListItem);

                const discountListItem = document.createElement('li');
                discountListItem.innerHTML = `优惠券: ${couponData[brandHrefId]["min_order_amount"]}-${couponData[brandHrefId]["coupon_amount"]}`;
                discountListItem.style.color = 'red';
                discountListItem.style.fontSize = '12px';
                couponContainer.appendChild(discountListItem);

              }
            }
          }
        });

        // Start observing the item for DOM changes
        observer.observe(item, {
          childList: true,
          subtree: true
        });
      });
    } catch (error) {
      console.error('处理商品数据时出错:', error);
    }
  }

  listener () {
    const config = { childList: true, subtree: true };

    const shopListObserver = new MutationObserver(mutations => {

      const addedItems = mutations
        .filter(mutation => mutation.type === 'childList')
        .flatMap(mutation => Array.from(mutation.addedNodes))
        .filter(node => node.tagName === 'DIV' && !node.className)
        .flatMap(div => Array.from(div.children))
        .filter(Boolean);

      if (addedItems.length > 0) {
        console.log('检测到新商品列表项:', addedItems.length);
        this.addCouponData(addedItems);
      }
    });

    const shopList = document.getElementById('shop_list');
    if (shopList) {
      shopListObserver.observe(shopList, config);
      console.log('商品列表观察器已启动');
    } else {
      console.warn('未找到商品列表容器');
    }
  }

  async checkForUpdates () {
    try {
      const response = await fetch(this.runTimeUrl + '?' + Date.now());
      const currentTime = await response.text();
      const config = this.configManager.getConfig();

      console.log('Checking update time:', currentTime, 'vs', config.lastUpdateTime);

      if (currentTime !== config.lastUpdateTime) {
        console.log('New update available, fetching coupon data...');
        const couponResponse = await fetch(this.couponUrl + '?' + Date.now());
        const couponData = await couponResponse.json();

        // 更新配置
        config.lastUpdateTime = currentTime;
        config.couponData = couponData;
        await this.configManager.save();

        console.log('Coupon data updated:', couponData);
        this.updateCouponDialog(currentTime);
      } else {
        console.log('No updates available');
      }
    } catch (error) {
      console.error('Error checking for updates:', error);
    }
  }

  updateCouponDialog () {
    const time = this.configManager.getConfig().lastUpdateTime
    console.log('Updating coupon dialog with time:', time);
    const dialog = document.querySelector('.header_left_title_Hlg2C');
    if (dialog) {
      console.log('Found dialog element');
      // 检查是否已经添加了时间信息
      const lastUpdateTime = `|优惠信息更新时间: ${time}`
      if (!dialog.innerHTML.includes('|')) {
        dialog.innerHTML += lastUpdateTime;
        console.log('Added time div to dialog');
      } else {
        dialog.innerHTML = dialog.innerHTML.split('|')[0] + lastUpdateTime;
        console.log('Time div already exists');
      }
    } else {
      console.log('Dialog element not found');
    }
  }
}


// 主程序入口
(async function () {
  "use strict";

  if (window.isScriptLoaded) {
    return;
  }
  window.isScriptLoaded = true;

  // 初始化配置管理器
  const configManager = new ConfigManager();
  await configManager.load();
  await configManager.initMenu();

  // 初始化UI增强
  const uiEnhancer = new UIEnhancer(configManager);
  uiEnhancer.init();

  const discountHandler = new DiscountHandler(configManager);
  discountHandler.init();

  document.documentElement.style.touchAction = "none";
  window.addEventListener("popstate", parseTabParams);


  let touchHandler = null;
  let lastUrl = "";


  /**
   * 移除现有的事件监听器
   */
  function removeEventListeners () {
    if (touchHandler) {
      touchHandler.destroy();
      touchHandler = null;
      return true;
    }
    return false;
  }

  /**
   * 初始化触摸事件处理
   * @param {Element} element - 要监听触摸事件的目标元素
   */
  function initTouchHandler (element) {
    // 先移除现有的事件监听
    removeEventListeners();

    if (element) {
      console.log("正在初始化触摸处理器,目标元素:", element);
      touchHandler = new TouchEventHandler(element, configManager);
      const success = touchHandler.init();

      if (success) {
        // 添加触摸调试信息
        // element.addEventListener('touchstart', (e) => console.log('Native touchstart:', e), false);
        console.log("触摸事件处理已初始化");
        return true;
      }
    }

    return false;
  }

  /**
   * 解析 URL 中的 tab 参数并匹配 div 和 iframe
   */
  function parseTabParams () {
    if (configManager.getConfig().enablePan && configManager.getConfig().enablePinch && configManager.getConfig().enablePress) {
      const currentUrl = window.location.href;

      // 如果 URL 没变,则不执行解析
      if (currentUrl === lastUrl) {
        console.log("URL 未变化,跳过执行");
        return;
      }

      // 更新上次的 URL 记录
      lastUrl = currentUrl;
      console.log("检测到 URL 变化:", currentUrl);

      // 解析 tab 参数
      const hash = window.location.hash;
      const tabMatch = hash.match(/tab=([^&#]*)/);

      if (tabMatch) {
        const tabList = tabMatch[1].split("|");
        console.log("Tab 参数解析为列表:", tabList);

        // 过滤出以 "*" 开头的项,并去除 "*"
        const starredTabs = tabList
          .filter((tab) => tab.startsWith("*"))
          .map((tab) => tab.substring(1));

        if (starredTabs.length > 0) {
          console.log('以 "*" 开头的项:', starredTabs);

          // 调用单独的函数解析 div 和 iframe
          const rootElement = parseDivAndIframe(starredTabs);
          if (rootElement) {
            console.log("解析成功,初始化触摸事件处理");
            initTouchHandler(rootElement);
          } else {
            console.log("未找到符合条件的元素,无法初始化触摸事件");
          }
        }
      } else {
        console.log("未找到 tab 参数");
      }
    }
  }

  /**
   * 在 #tabbar_bodies 下查找 div 元素,并匹配 uuid
   * 如果找到符合条件的 div,则继续查找其内部的 iframe,获取 #root 或 #canvas
   * @param {Array} starredTabs - 以 "*" 开头的 tab 列表(去除了 "*")
   * @returns {Element|boolean} 如果找到匹配的元素返回该元素,未找到返回 false
   */
  function parseDivAndIframe (starredTabs) {
    // 获取 #tabbar_bodies 下的所有 div
    const divs = document.querySelectorAll("#tabbar_bodies div");

    if (divs.length === 0) {
      console.log("未找到任何 div");
      return false;
    }

    // 遍历 div 并匹配 uuid
    for (let div of divs) {
      const uuid = div.getAttribute("uuid"); // 获取 uuid 属性
      if (uuid && starredTabs.includes(uuid)) {
        console.log("匹配的 div:", div);

        // 查找 div 内的 iframe
        const iframe = div.querySelector("iframe");
        if (iframe) {
          console.log("找到匹配的 iframe:", iframe);

          // 尝试访问 iframe 的内容
          try {
            const iframeDoc =
              iframe.contentDocument || iframe.contentWindow.document;

            // 查找 #root 或 #canvas
            let rootElement = iframeDoc.querySelector("#root");
            if (!rootElement) {
              rootElement = iframeDoc.querySelector("#canvas");
            }

            if (rootElement) {
              console.log("找到目标元素:", rootElement);
              return rootElement;
            } else {
              console.log("未找到 #root 或 #canvas");
            }
          } catch (error) {
            console.error("无法访问 iframe 内容:", error);
          }
        } else {
          console.log("匹配的 div 内未找到 iframe");
        }
      }
    }

    return false; // 如果没有找到匹配的 div 和 iframe
  }
})();