Greasy Fork is available in English.

Copy token from skland for ark.yituliu.cn

Script to copy skland token to ark.yituliu.cn

// ==UserScript==
// @name               Copy token from skland for ark.yituliu.cn
// @name:zh-CN         一键复制明日方舟一图流所需的森空岛token
// @namespace          ling921
// @version            0.2.0
// @description        Script to copy skland token to ark.yituliu.cn
// @description:zh-CN  脚本用于复制森空岛token到明日方舟一图流中使用,并支持Shift+C快捷键
// @author             ling921
// @match              https://www.skland.com/*
// @icon               https://ark.yituliu.cn/favicon.ico
// @grant              none
// @run-at             document-idle
// @tag                utilities
// @tag                game
// @license            MIT
// ==/UserScript==

/**
 * 通知管理器类
 */
class NotificationManager {
  static #instance = null;
  static #maxNotifications = 3;
  static #defaultDuration = 3000;
  static #queue = [];
  static #active = new Set();
  static #container = null;

  /**
   * 获取通知管理器实例
   * @param {{
   *  maxNotifications?: number,
   *  duration?: number
   * }} options - 配置选项
   */
  static getInstance(options = {}) {
    if (!NotificationManager.#instance) {
      if (options.maxNotifications && options.maxNotifications > 0) {
        NotificationManager.#maxNotifications = options.maxNotifications;
      }
      if (options.duration && options.duration > 0) {
        NotificationManager.#defaultDuration = options.duration;
      }
      NotificationManager.#instance = new NotificationManager(options);
    }
    return NotificationManager.#instance;
  }

  constructor() {
    if (NotificationManager.#instance) {
      return NotificationManager.#instance;
    }

    NotificationManager.#container = document.getElementById(
      "notification-container"
    );
    if (!NotificationManager.#container) {
      const style = document.createElement("style");
      style.textContent = `
        #notification-container {
          position: fixed;
          top: 20px;
          left: 0;
          right: 0;
          display: flex;
          flex-direction: column;
          align-items: center;
          gap: 12px;
          pointer-events: none;
          z-index: 10000;
        }
        
        #notification-container .notification {
          position: relative;
          padding: 10px 25px;
          padding-right: 35px !important;
          border-radius: 20px;
          font-size: 14px;
          color: white;
          font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
          opacity: 0;
          transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
          text-align: center;
          backdrop-filter: blur(8px);
          -webkit-backdrop-filter: blur(8px);
          transform: translateY(-20px);
          pointer-events: auto;
        }

        #notification-container .notification:hover {
          filter: brightness(1.1);
        }

        #notification-container .notification .close {
          position: absolute !important;
          right: 10px !important;
          top: 50% !important;
          transform: translateY(-50%) !important;
          width: 16px !important;
          height: 16px !important;
          cursor: pointer !important;
          opacity: 0.7 !important;
          transition: opacity 0.2s !important;
          display: flex !important;
          align-items: center !important;
          justify-content: center !important;
          user-select: none !important;
        }

        #notification-container .notification .close:hover {
          opacity: 1 !important;
        }

        #notification-container .success {
          background-color: #52c41a !important;
          box-shadow: 0 4px 12px rgba(82, 196, 26, 0.3) !important;
          border-color: rgba(255, 255, 255, 0.2) !important;
        }

        #notification-container .info {
          background-color: #1890ff !important;
          box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3) !important;
          border-color: rgba(255, 255, 255, 0.15) !important;
        }

        #notification-container .warning {
          background-color: #faad14 !important;
          box-shadow: 0 4px 12px rgba(250, 173, 20, 0.3) !important;
          border-color: rgba(255, 255, 255, 0.1) !important;
        }

        #notification-container .error {
          background-color: #ff4d4f !important;
          box-shadow: 0 4px 12px rgba(255, 77, 79, 0.3) !important;
          border-color: rgba(255, 255, 255, 0.1) !important;
        }
      `;
      document.head.appendChild(style);

      NotificationManager.#container = document.createElement("div");
      NotificationManager.#container.id = "notification-container";
      document.body.appendChild(NotificationManager.#container);
    }

    NotificationManager.#instance = this;
  }

  success(message, duration) {
    this.#show(message, "success", duration);
  }

  info(message, duration) {
    this.#show(message, "info", duration);
  }

  warning(message, duration) {
    this.#show(message, "warning", duration);
  }

  error(message, duration) {
    this.#show(message, "error", duration);
  }

  #show(message, type, duration) {
    if (!duration || duration <= 0) {
      duration = NotificationManager.#defaultDuration;
    }
    NotificationManager.#queue.push({ message, type, duration });
    NotificationManager.#processQueue();
  }

  static #processQueue() {
    if (
      NotificationManager.#queue.length === 0 ||
      NotificationManager.#active.size >= NotificationManager.#maxNotifications
    ) {
      return;
    }

    const { message, type, duration } = NotificationManager.#queue.shift();
    NotificationManager.#showNotification(message, type, duration);

    if (NotificationManager.#queue.length > 0) {
      NotificationManager.#processQueue();
    }
  }

  static #showNotification(
    message,
    type,
    duration = NotificationManager.#defaultDuration
  ) {
    const notify = document.createElement("div");
    notify.classList.add("notification", type);

    const messageText = document.createElement("span");
    messageText.textContent = message;
    notify.appendChild(messageText);

    const removeNotify = () => {
      notify.style.opacity = "0";
      notify.style.transform = "translateY(-20px)";
      setTimeout(() => {
        notify.remove();
        NotificationManager.#active.delete(notify);
        NotificationManager.#processQueue();
      }, 300);
    };

    const closeBtn = document.createElement("div");
    closeBtn.classList.add("close");
    closeBtn.innerHTML = "✕";
    closeBtn.onclick = removeNotify;
    notify.appendChild(closeBtn);

    let timer;

    notify.addEventListener("mouseenter", () => {
      clearTimeout(timer);
    });

    notify.addEventListener("mouseleave", () => {
      timer = setTimeout(removeNotify, duration);
    });

    NotificationManager.#container.appendChild(notify);
    NotificationManager.#active.add(notify);

    requestAnimationFrame(() => {
      notify.style.opacity = "1";
      notify.style.transform = "translateY(0)";
    });

    timer = setTimeout(removeNotify, duration);
  }
}

/**
 * 防抖复制令牌
 */
const debouncedCopyToken = debounce(copyToken, 300);

/**
 * 消息通知
 */
const message = NotificationManager.getInstance();

(function () {
  ("use strict");

  // 添加按钮
  const button = createButton();
  document.body.appendChild(button);

  // 添加快捷键
  document.addEventListener("keydown", (event) => {
    if (event.shiftKey && event.key.toLowerCase() === "c") {
      event.preventDefault();
      debouncedCopyToken();
    }
  });
})();

/**
 * 创建按钮
 * @returns {HTMLElement} - 按钮元素
 */
function createButton() {
  // 创建按钮
  const button = document.createElement("div", { class: "copy-button" });
  button.innerHTML = `
      <div class="main-text">复制</div>
      <div class="shortcut">Shift + C</div>
  `;
  button.style.cssText = `
    position: fixed;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    background-color: rgba(71, 120, 224, 0.85);
    color: rgba(255, 255, 255, 0.95);
    border: none;
    padding: 10px 20px;
    border-radius: 20px 0 0 20px;
    cursor: pointer;
    font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
    font-size: 14px;
    transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
    z-index: 9999;
    overflow: hidden;
    white-space: nowrap;
    text-align: center;
    line-height: 1.4;
    box-shadow: 0 2px 8px rgba(71, 120, 224, 0.2);
    min-width: 80px;
  `;

  // 添加内部元素的样式
  const style = document.createElement("style");
  style.textContent = `
    .copy-button {
      display: flex;
      flex-direction: column;
      align-items: center;
      transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
      user-select: none;
    }
    .main-text {
      font-size: 14px;
      transition: min-width 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
      width: 100%;
    }
    .shortcut {
      font-size: 10px;
      opacity: 0.8;
      margin-top: 2px;
    }
    .copy-button {
      transform-origin: right center;
    }
    .copy-button:hover {
      min-width: 140px;
      padding-right: 25px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    }
    .copy-button:active {
      transform: translateY(-50%) scale(0.98);
    }
  `;
  document.head.appendChild(style);

  let isAnimating = false;
  let currentAnimation = null; // 用于存储当前动画的定时器
  const fullText = "一图流令牌";
  const typingSpeed = 50; // 打字速度(ms)

  /**
   * 打字动画
   * @param {HTMLElement} element - 要打字的元素
   * @param {string} text - 要打字的文本
   * @param {number} currentIndex - 当前索引
   * @returns {void}
   */
  function typeText(element, text, currentIndex = 0) {
    if (currentAnimation) {
      clearTimeout(currentAnimation);
    }

    if (currentIndex <= text.length) {
      element.textContent = "复制" + text.slice(0, currentIndex);
      currentAnimation = setTimeout(() => {
        typeText(element, text, currentIndex + 1);
      }, typingSpeed);
    } else {
      isAnimating = false;
      currentAnimation = null;
    }
  }

  /**
   * 删除文本
   * @param {HTMLElement} element - 要删除文本的元素
   * @param {string} text - 要删除的文本
   * @param {number} currentIndex - 当前索引
   * @returns {void}
   */
  function deleteText(element, text, currentIndex = text.length) {
    if (currentAnimation) {
      clearTimeout(currentAnimation);
    }

    if (currentIndex >= 0) {
      element.textContent = "复制" + text.slice(0, currentIndex);
      currentAnimation = setTimeout(() => {
        deleteText(element, text, currentIndex - 1);
      }, typingSpeed);
    } else {
      isAnimating = false;
      currentAnimation = null;
    }
  }

  // 添加悬停效果
  button.addEventListener("mouseenter", () => {
    if (currentAnimation) {
      clearTimeout(currentAnimation);
    }
    isAnimating = true;
    button.style.backgroundColor = "rgba(86, 146, 255, 0.95)";
    typeText(button.querySelector(".main-text"), fullText);
  });

  button.addEventListener("mouseleave", () => {
    if (currentAnimation) {
      clearTimeout(currentAnimation);
    }
    isAnimating = true;
    button.style.backgroundColor = "rgba(71, 120, 224, 0.85)";
    deleteText(button.querySelector(".main-text"), fullText);
  });

  // 添加点击事件
  button.addEventListener("click", debouncedCopyToken);

  return button;
}

/**
 * 复制令牌
 * @returns {void}
 */
function copyToken() {
  try {
    const skOauthCredKey = localStorage.getItem("SK_OAUTH_CRED_KEY");
    const skTokenCacheKey = localStorage.getItem("SK_TOKEN_CACHE_KEY");

    if (!skOauthCredKey || !skTokenCacheKey) {
      const missingKeys = [];
      if (!skOauthCredKey) missingKeys.push("SK_OAUTH_CRED_KEY");
      if (!skTokenCacheKey) missingKeys.push("SK_TOKEN_CACHE_KEY");

      message.error(`缺少必要的密钥:${missingKeys.join("、")}`);
      return;
    }

    const combinedData = `${skOauthCredKey},${skTokenCacheKey}`;

    navigator.clipboard
      .writeText(combinedData)
      .then(() => message.success("令牌复制成功!"))
      .catch((err) => {
        console.error("复制失败!", err);
        message.error("复制失败!");
      });
  } catch (error) {
    console.error("操作过程出现错误!", error);
    message.error("操作过程出现错误!");
  }
}

/**
 * 防抖函数
 * @param {Function} func - 要防抖的函数
 * @param {number} wait - 等待时间(毫秒)
 * @returns {Function} - 防抖后的函数
 */
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}