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