Files

1302 lines
46 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { api } from "../../scripts/api.js";
class BaseNode extends LGraphNode {
static defaultComfyClass = "BaseNode";
constructor(title, comfyClass) {
super(title);
this.isVirtualNode = false;
this.configuring = false;
this.__constructed__ = false;
this.widgets = this.widgets || [];
this.properties = this.properties || {};
this.comfyClass = comfyClass || this.constructor.comfyClass || BaseNode.defaultComfyClass;
setTimeout(() => {
this.checkAndRunOnConstructed();
});
}
checkAndRunOnConstructed() {
if (!this.__constructed__) {
this.onConstructed();
}
return this.__constructed__;
}
onConstructed() {
if (this.__constructed__) return false;
this.type = this.type ?? undefined;
this.__constructed__ = true;
return this.__constructed__;
}
configure(info) {
this.configuring = true;
super.configure(info);
for (const w of this.widgets || []) {
w.last_y = w.last_y || 0;
}
this.configuring = false;
}
static setUp() {
if (!this.type) {
throw new Error(`Missing type for ${this.name}: ${this.title}`);
}
LiteGraph.registerNodeType(this.type, this);
if (this._category) {
this.category = this._category;
}
}
}
class ImageDisplayNode extends BaseNode {
static type = "Advertisement";
static title = "🎈Advertisement";
static category = "🎈LAOGOU/Utils";
static _category = "🎈LAOGOU/Utils";
static comfyClass = "Advertisement";
constructor(title = ImageDisplayNode.title) {
super(title, ImageDisplayNode.comfyClass);
this.comfyClass = "Advertisement";
this.resizable = true;
this.size = [200, 200];
this.media = null;
this.mediaType = null;
this.isVirtualNode = true;
// 设置节点属性
this.properties = {
borderRadius: 0,
backgroundColor: "transparent",
padding: 0,
fitMode: "contain",
flipH: false,
flipV: false,
autoplay: true,
loop: true,
mediaSource: "",
volume: 0,
};
// 移除默认标题栏
this.flags = {
showTitle: false
};
// 设置节点颜色为透明
this.color = "transparent";
this.bgcolor = "transparent";
this.onConstructed();
this.isDraggingOver = false;
this.gifPlayer = null;
this.scriptPath = import.meta.url;
}
setProperty(name, value) {
super.setProperty(name, value);
if (name === "mediaSource" && value) {
this.handleMediaSource(value).catch(error => {
console.error("处理媒体源失败:", error);
alert("处理媒体源失败: " + error.message);
});
}
}
async handleMediaSource(source) {
try {
let file;
if (source.startsWith('http://') || source.startsWith('https://')) {
// 处理网络地址
console.log('开始下载网络文件:', source);
// 通过后端代理下载
const response = await api.fetchApi('/proxy_download', {
method: 'POST',
body: JSON.stringify({
url: source
})
});
const data = await response.json();
if (data.status === "error") {
throw new Error(data.message || '下载文件失败');
}
// 获取已下载的文件
const fileResponse = await fetch(this.getViewPath(`input/image_display/${data.name}`));
if (!fileResponse.ok) {
throw new Error('无法加载已下载的文件');
}
const blob = await fileResponse.blob();
file = new File([blob], data.name, { type: this.getMimeTypeFromUrl(data.name) });
} else {
const cleanPath = source.trim()
.replace(/[\r\n]+/g, '')
.replace(/^["']|["']$/g, ''); // 去除开头和结尾的引号
// 处理相对路径
let fullPath = cleanPath;
const isRelativePath = !cleanPath.match(/^([A-Za-z]:|\/)/) &&
!cleanPath.includes('/input/image_display/') &&
!cleanPath.includes('\\input\\image_display\\');
if (isRelativePath) {
// 如果是相对路径,去掉可能存在的 './'
fullPath = cleanPath.startsWith('./') ? cleanPath.slice(2) : cleanPath;
}
console.log('开始处理文件:', fullPath, isRelativePath ? '(相对路径)' : '(绝对路径)');
// 检查文件是否已经在目标文件夹中
const fileName = fullPath.split(/[\\/]/).pop();
const targetPath = `input/image_display/${fileName}`;
// 如果文件已经在目标文件夹中,直接使用
if (cleanPath.includes('/input/image_display/') || cleanPath.includes('\\input\\image_display\\')) {
console.log('文件已在目标文件夹中,直接使用');
const fileResponse = await fetch(this.getViewPath(`input/image_display/${fileName}`));
if (!fileResponse.ok) {
throw new Error('无法加载文件');
}
const blob = await fileResponse.blob();
const mimeType = this.getMimeTypeFromUrl(fileName);
file = new File([blob], fileName, { type: mimeType });
if (file.type.startsWith('video/')) {
await this.loadVideo(file);
} else if (file.type.startsWith('image/')) {
if (file.type === 'image/gif') {
await this.loadGif(file);
} else {
await this.loadImage(file);
}
}
return;
}
const response = await api.fetchApi('/upload_from_path', {
method: 'POST',
body: JSON.stringify({
path: fullPath,
subfolder: 'image_display',
type: 'input',
relative: isRelativePath
})
});
const data = await response.json();
if (data.status === "error") {
throw new Error(data.message || '处理本地文件失败');
}
// 获取复制后的文件
const fileResponse = await fetch(this.getViewPath(`input/image_display/${fileName}`));
if (!fileResponse.ok) {
throw new Error('无法加载文件');
}
const blob = await fileResponse.blob();
const mimeType = this.getMimeTypeFromUrl(fileName);
file = new File([blob], fileName, { type: mimeType });
}
// 保存和加载媒体
console.log('开始保存文件:', file.name);
const savedPath = await this.saveMediaToTemp(file);
if (!savedPath) {
throw new Error('保存文件失败');
}
// 根据文件类型加载媒体
if (file.type.startsWith('video/')) {
await this.loadVideo(file);
} else if (file.type.startsWith('image/')) {
if (file.type === 'image/gif') {
await this.loadGif(file);
} else {
await this.loadImage(file);
}
}
} catch (error) {
console.error('处理媒体源失败:', error);
throw error;
}
}
getFileNameFromUrl(source) {
if (source.startsWith('http')) {
const urlParts = source.split(/[#?]/)[0].split('/');
return decodeURIComponent(urlParts.pop() || 'unknown');
} else {
// 处理本地路径
return source.split(/[\\/]/).pop();
}
}
getMimeTypeFromUrl(source) {
const ext = source.split('.').pop().toLowerCase();
const mimeTypes = {
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
'mp4': 'video/mp4',
'webm': 'video/webm'
};
return mimeTypes[ext] || 'application/octet-stream';
}
async loadGifLibrary() {
try {
const basePath = this.scriptPath.substring(0, this.scriptPath.lastIndexOf('/'));
const libPath = `${basePath}/lib/libgif.js`;
console.log('Loading GIF library from:', libPath); // 调试用
const script = document.createElement('script');
script.src = libPath;
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = (e) => {
console.error('Failed to load GIF library:', e); // 调试用
reject(e);
};
document.head.appendChild(script);
});
console.log('GIF library loaded successfully'); // 调试用
} catch (error) {
console.error('Error loading GIF library:', error);
return false;
}
return true;
}
draw(ctx) {
ctx.save();
this.color = "transparent";
this.bgcolor = "transparent";
if (this.properties.backgroundColor !== "transparent") {
ctx.beginPath();
const borderRadius = this.properties.borderRadius || 0;
ctx.roundRect(0, 0, this.size[0], this.size[1], [borderRadius]);
ctx.fillStyle = this.properties.backgroundColor;
ctx.fill();
}
if (this.media) {
const padding = this.properties.padding || 0;
const drawWidth = this.size[0] - (padding * 2);
const drawHeight = this.size[1] - (padding * 2);
let mediaWidth, mediaHeight;
if (this.mediaType === 'video') {
mediaWidth = this.media.videoWidth;
mediaHeight = this.media.videoHeight;
} else {
mediaWidth = this.media.width;
mediaHeight = this.media.height;
}
const mediaRatio = mediaWidth / mediaHeight;
const nodeRatio = drawWidth / drawHeight;
let finalWidth = drawWidth;
let finalHeight = drawHeight;
let x = padding;
let y = padding;
if (this.properties.fitMode === "contain") {
if (mediaRatio > nodeRatio) {
finalHeight = drawWidth / mediaRatio;
y = padding + (drawHeight - finalHeight) / 2;
} else {
finalWidth = drawHeight * mediaRatio;
x = padding + (drawWidth - finalWidth) / 2;
}
} else if (this.properties.fitMode === "cover") {
if (mediaRatio > nodeRatio) {
finalWidth = drawHeight * mediaRatio;
x = padding + (drawWidth - finalWidth) / 2;
} else {
finalHeight = drawWidth / mediaRatio;
y = padding + (drawHeight - finalHeight) / 2;
}
}
if (this.properties.flipH || this.properties.flipV) {
ctx.save();
ctx.translate(x + finalWidth / 2, y + finalHeight / 2);
ctx.scale(this.properties.flipH ? -1 : 1, this.properties.flipV ? -1 : 1);
ctx.translate(-(x + finalWidth / 2), -(y + finalHeight / 2));
}
// 绘制媒体
ctx.drawImage(this.media, x, y, finalWidth, finalHeight);
if (this.properties.flipH || this.properties.flipV) {
ctx.restore();
}
if (this.mediaType === 'video' || this.mediaType === 'gif') {
requestAnimationFrame(() => {
if (this.graph) {
this.graph.setDirtyCanvas(true);
}
});
}
if (this.mediaType === 'gif' && this.gifPlayer) {
requestAnimationFrame(() => {
if (this.graph) {
this.graph.setDirtyCanvas(true);
}
});
}
if (this.mediaType === 'video' && !this.properties.autoplay) {
// 绘制播放图标
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.beginPath();
ctx.arc(this.size[0]/2, this.size[1]/2, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.moveTo(this.size[0]/2 - 8, this.size[1]/2 - 10);
ctx.lineTo(this.size[0]/2 - 8, this.size[1]/2 + 10);
ctx.lineTo(this.size[0]/2 + 8, this.size[1]/2);
ctx.closePath();
ctx.fill();
}
} else {
ctx.fillStyle = "#666";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("拖放媒体文件到此处", this.size[0] / 2, this.size[1] / 2);
ctx.font = "12px Arial";
ctx.fillText("支持图片、GIF和视频", this.size[0] / 2, this.size[1] / 2 + 20);
}
ctx.restore();
}
onDragOver(e, local_pos, canvas) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
// 更新拖放状态
if (!this.isDraggingOver) {
this.isDraggingOver = true;
this.graph.setDirtyCanvas(true);
}
return true;
}
onDragLeave(e) {
this.isDraggingOver = false;
this.graph.setDirtyCanvas(true);
}
onDragDrop(e, local_pos, canvas) {
e.preventDefault();
e.stopPropagation();
this.isDraggingOver = false;
const files = e.dataTransfer.files;
if (files && files.length > 0) {
const file = files[0];
const type = file.type.toLowerCase();
if (this.media) {
if (this.mediaType === 'video') {
this.media.pause();
URL.revokeObjectURL(this.media.src);
}
this.media = null;
}
if (type.startsWith('video/')) {
this.loadVideo(file);
} else if (type.startsWith('image/')) {
if (type === 'image/gif') {
this.loadGif(file);
} else {
this.loadImage(file);
}
}
}
this.graph.setDirtyCanvas(true);
return true;
}
async saveMediaToTemp(file, subfolder = 'image_display') {
try {
this.originalFileName = file.name;
const formData = new FormData();
formData.append('image', file);
formData.append('type', 'input');
formData.append('subfolder', subfolder);
formData.append('overwrite', 'true');
const response = await api.fetchApi('/upload/image', {
method: 'POST',
body: formData
});
const responseData = await response.json();
if (responseData?.name) {
const path = `input/${subfolder}/${responseData.name}`;
return path;
}
return null;
} catch (error) {
console.error('保存文件失败:', error);
return null;
}
}
serialize() {
const data = super.serialize();
if (this.media && this.tempFilePath) {
data.mediaType = this.mediaType;
data.tempFilePath = this.tempFilePath;
}
return data;
}
getViewPath(filePath) {
const filename = filePath.split('/').pop();
const subfolder = filePath.split('/')[1];
return `/view?filename=${filename}&type=input&subfolder=${subfolder}`;
}
createMediaElement(type, viewPath, autoplay = false, loop = false) {
switch(type) {
case 'video':
const video = document.createElement('video');
video.autoplay = autoplay;
video.loop = loop;
video.muted = true;
video.volume = this.properties.volume;
video.playsInline = true;
video.src = viewPath;
return video;
case 'gif':
const tempImg = document.createElement('img');
tempImg.src = viewPath;
return tempImg;
default: // 'image'
const img = new Image();
img.src = viewPath;
return img;
}
}
configure(info) {
super.configure(info);
if (!info.mediaType || !info.tempFilePath) return;
this.tempFilePath = info.tempFilePath;
const viewPath = this.getViewPath(this.tempFilePath);
if (info.mediaType === 'gif') {
this.handleGif(viewPath);
return;
}
const element = this.createMediaElement(
info.mediaType,
viewPath,
this.properties.autoplay,
this.properties.loop
);
const loadHandler = info.mediaType === 'video' ? 'onloadedmetadata' : 'onload';
element[loadHandler] = () => {
this.media = element;
this.mediaType = info.mediaType;
if (info.mediaType === 'video' && this.properties.autoplay) {
element.play().catch(console.error);
}
this.graph?.setDirtyCanvas(true);
};
if (this.mediaType === 'video') {
this.updateAudioSettings();
}
}
async handleGif(viewPath) {
if (typeof SuperGif === 'undefined') {
await this.loadGifLibrary();
}
const tempImg = this.createMediaElement('gif', viewPath);
this.gifPlayer = new SuperGif({
gif: tempImg,
auto_play: true,
loop_mode: true
});
return new Promise((resolve) => {
this.gifPlayer.load(() => {
this.media = this.gifPlayer.get_canvas();
this.mediaType = 'gif';
this.graph?.setDirtyCanvas(true);
resolve();
});
});
}
async loadMedia(file, type) {
console.log(`开始加载${type}:`, file.name);
const tempPath = await this.saveMediaToTemp(file);
if (!tempPath) {
console.error(`Failed to save ${type}`);
return;
}
this.tempFilePath = tempPath;
const viewPath = this.getViewPath(tempPath);
console.log('构建的访问路径:', viewPath);
if (type === 'gif') {
await this.handleGif(viewPath);
return;
}
const element = this.createMediaElement(
type,
viewPath,
this.properties.autoplay,
this.properties.loop
);
const loadHandler = type === 'video' ? 'onloadedmetadata' : 'onload';
element[loadHandler] = () => {
this.media = element;
this.mediaType = type;
if (type === 'video' && this.properties.autoplay) {
element.play().catch(e => console.warn("Video autoplay failed:", e));
}
this.graph.setDirtyCanvas(true);
};
}
loadImage(file) { return this.loadMedia(file, 'image'); }
loadGif(file) { return this.loadMedia(file, 'gif'); }
updateAudioSettings() {
if (this.media && this.mediaType === 'video') {
console.log('更新音频设置,当前音量:', this.properties.volume);
this.media.volume = this.properties.volume;
this.media.muted = false;
}
}
loadVideo(file) {
return this.loadMedia(file, 'video').then(() => {
if (this.media) {
this.media.muted = true;
this.media.play().then(() => {
if (this.properties.volume > 0) {
this.media.muted = false;
this.media.volume = this.properties.volume;
}
}).catch(console.error);
}
});
}
clone() {
const cloned = super.clone();
if (this.mediaType && this.tempFilePath) {
cloned.mediaType = this.mediaType;
cloned.tempFilePath = this.tempFilePath;
const viewPath = this.getViewPath(this.tempFilePath);
if (this.mediaType === 'gif') {
cloned.handleGif(viewPath);
return cloned;
}
const element = this.createMediaElement(
this.mediaType,
viewPath,
this.properties.autoplay,
this.properties.loop
);
const loadHandler = this.mediaType === 'video' ? 'onloadedmetadata' : 'onload';
element[loadHandler] = () => {
cloned.media = element;
if (this.mediaType === 'video' && cloned.properties.autoplay) {
element.play().catch(console.error);
}
cloned.graph?.setDirtyCanvas(true);
};
}
return cloned;
}
getExtraMenuOptions(canvas, options) {
const node = this;
const volumeId = `volume-${Date.now()}`;
const fileInputId = `file-input-${Date.now()}`;
// 创建隐藏的文件输入框
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = fileInputId;
fileInput.style.display = 'none';
fileInput.accept = 'video/*,audio/*,image/*,.gif';
document.body.appendChild(fileInput);
// 监听文件选择
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
// 根据文件类型调用相应的加载方法
if (file.type.startsWith('video/')) {
await this.loadVideo(file);
} else if (file.type.startsWith('audio/')) {
await this.loadMedia(file, 'audio');
} else if (file.type.startsWith('image/')) {
if (file.type === 'image/gif') {
await this.loadGif(file);
} else {
await this.loadImage(file);
}
}
canvas.setDirty(true);
} catch (error) {
console.error('加载媒体文件失败:', error);
} finally {
// 清理文件输入框
document.body.removeChild(fileInput);
}
});
options.unshift( // 在开头添加上传选项
{
content: "上传媒体文件",
callback: () => {
fileInput.click();
}
},
null, // 添加分隔线
);
options.push(
{
content: "水平翻转",
callback: () => {
this.properties.flipH = !this.properties.flipH;
canvas.setDirty(true);
}
},
{
content: "垂直翻转",
callback: () => {
this.properties.flipV = !this.properties.flipV;
canvas.setDirty(true);
}
},
null,
...(this.mediaType === 'video' ? [
{
content: this.properties.autoplay ? "暂停自动播放" : "启用自动播放",
callback: () => {
this.properties.autoplay = !this.properties.autoplay;
if (this.media) {
if (this.properties.autoplay) {
this.media.play().catch(e => console.error('播放失败:', e));
} else {
this.media.pause();
}
this.media.autoplay = this.properties.autoplay;
}
canvas.setDirty(true);
}
},
{
content: this.properties.loop ? "禁用循环播放" : "启用循环播放",
callback: () => {
this.properties.loop = !this.properties.loop;
if (this.media) {
this.media.loop = this.properties.loop;
if (this.properties.loop && this.properties.autoplay) {
this.media.play().catch(e => console.error('播放失败:', e));
}
}
canvas.setDirty(true);
}
},
null,
{
content: `<div style="padding: 5px">
<div id="${volumeId}-label">音量: ${Math.round((this.properties.volume || 0) * 100)}%</div>
<input type="range"
id="${volumeId}-slider"
min="0"
max="1"
step="0.01"
value="${this.properties.volume}"
style="width: 150px"
onmousedown="event.stopPropagation()"
/>
</div>`,
isHTML: true,
callback: () => false
}
] : []),
null,
{
content: "清除媒体",
callback: () => {
// 处理视频媒体
if (this.media && this.mediaType === 'video') {
this.media.pause();
URL.revokeObjectURL(this.media.src);
}
if (this.properties.mediaSource && this.properties.mediaSource.startsWith('blob:')) {
URL.revokeObjectURL(this.properties.mediaSource);
}
// 重置所有相关属性
this.media = null;
this.mediaType = null;
this.properties.mediaSource = ""; // 清除 URL
// 更新显示
this.updateContent();
canvas.setDirty(true);
}
}
);
setTimeout(() => {
const slider = document.getElementById(`${volumeId}-slider`);
const label = document.getElementById(`${volumeId}-label`);
if (slider && label) {
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
node.properties.volume = value;
if (node.media) {
node.media.volume = value;
node.media.muted = false;
}
label.textContent = `音量: ${Math.round(value * 100)}%`;
node.graph.setDirtyCanvas(true);
e.stopPropagation();
});
}
}, 0);
}
onRemoved() {
if (this.mediaType === 'video' && this.media) {
this.media.pause();
URL.revokeObjectURL(this.media.src);
}
if (this.mediaType === 'gif') {
if (this.gifPlayer) {
// 停止 GIF 播放
this.gifPlayer.pause();
this.gifPlayer = null;
}
}
this.media = null;
this.mediaType = null;
this.isDraggingOver = false;
}
}
ImageDisplayNode.title_mode = LiteGraph.NO_TITLE;
ImageDisplayNode.collapsable = false;
ImageDisplayNode["@borderRadius"] = { type: "number" };
ImageDisplayNode["@backgroundColor"] = { type: "string" };
ImageDisplayNode["@padding"] = { type: "number" };
ImageDisplayNode["@fitMode"] = { type: "combo", values: ["contain", "cover", "stretch"] };
ImageDisplayNode["@volume"] = { type: "number", default: 0, min: 0, max: 1, step: 0.01 };
const oldDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function (node, ctx) {
if (node instanceof ImageDisplayNode) {
node.bgcolor = "transparent";
node.color = "transparent";
const v = oldDrawNode.apply(this, arguments);
node.draw(ctx);
return v;
}
return oldDrawNode.apply(this, arguments);
};
app.registerExtension({
name: "Advertisement",
registerCustomNodes() {
ImageDisplayNode.setUp();
},
});
class MediaPlayerNode extends BaseNode {
static type = "MediaPlayer";
static title = "🎈MediaPlayer";
static category = "🎈LAOGOU/Utils";
static _category = "🎈LAOGOU/Utils";
static comfyClass = "MediaPlayer";
constructor(title = MediaPlayerNode.title) {
super(title, MediaPlayerNode.comfyClass);
this.resizable = true;
this.size = [320, 240];
this.isVirtualNode = true;
this.shape = LiteGraph.ROUND_SHAPE;
this.serialize_widgets = true;
// 添加属性
this.addProperty("url", "", "string");
this.addProperty("theme", "light", "enum", {
values: ["light", "dark"]
});
const container = document.createElement('div');
const inner = document.createElement('div');
this.inner = inner;
container.append(inner);
inner.classList.add('media-player-preview');
container.style.cssText = `
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: white;
display: flex;
overflow: hidden; /* 防止容器本身出现滚动条 */
background: transparent; /* 设置为透明 */
`;
inner.style.cssText = `
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-y: auto; /* 只允许垂直滚动 */
overflow-x: hidden; /* 禁止水平滚动 */
background: transparent; /* 设置为透明 */
`;
this.html_widget = this.addDOMWidget('HTML', 'html', container, {
setValue: () => {},
getValue: () => {},
getMinHeight: () => this.size[1],
onDraw: () => {
this.html_widget.element.style.pointerEvents = 'all';
}
});
this.color = "#E0E0E0";
this.bgcolor = "#FFFFFF";
this.flags = {
showTitle: false
};
// 加载 marked 库
this.loadMarkedLibrary();
this.onConstructed();
this.updateContent();
this.applyTheme();
}
async loadMarkedLibrary() {
if (typeof marked !== 'undefined') return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = './lib/marked.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
applyTheme() {
const isDark = this.properties.theme === 'dark';
// 定义统一的颜色变量
const colors = {
dark: {
nodeBg: '#1E1E1E', // 节点背景
nodeColor: '#2D2D2D', // 节点边框
contentBg: '#1E1E1E', // 内容区背景
textColor: '#E0E0E0', // 文本颜色
headingColor: '#ffffff' // 标题颜色
},
light: {
nodeBg: '#FFFFFF',
nodeColor: '#E0E0E0',
contentBg: '#FFFFFF',
textColor: '#666666',
headingColor: '#333333'
}
};
const theme = isDark ? colors.dark : colors.light;
// 设置节点颜色
this.color = theme.nodeColor;
this.bgcolor = theme.nodeBg;
// 设置容器样式
if (this.inner) {
this.inner.style.background = theme.contentBg;
// 构建主题样式
const themeStyles = `
/* 深色主题样式 */
.dark-theme {
color: ${colors.dark.textColor} !important;
background: ${colors.dark.contentBg} !important;
}
.dark-theme a {
color: #58a6ff !important;
}
.dark-theme h1,
.dark-theme h2,
.dark-theme h3,
.dark-theme h4,
.dark-theme h5,
.dark-theme h6 {
color: ${colors.dark.headingColor} !important;
}
.dark-theme code {
color: #e6e6e6 !important;
background-color: ${colors.dark.nodeColor} !important;
}
.dark-theme pre {
background-color: ${colors.dark.nodeColor} !important;
}
.dark-theme blockquote {
color: #bebebe !important;
border-left-color: #4f4f4f !important;
}
.dark-theme table th,
.dark-theme table td {
border-color: #4f4f4f !important;
}
.dark-theme hr {
border-color: #4f4f4f !important;
}
/* 浅色主题样式 */
.markdown-body:not(.dark-theme) {
color: ${colors.light.textColor} !important;
background: ${colors.light.contentBg} !important;
}
.markdown-body:not(.dark-theme) h1,
.markdown-body:not(.dark-theme) h2,
.markdown-body:not(.dark-theme) h3,
.markdown-body:not(.dark-theme) h4,
.markdown-body:not(.dark-theme) h5,
.markdown-body:not(.dark-theme) h6 {
color: ${colors.light.headingColor} !important;
}
.markdown-body:not(.dark-theme) code {
color: #24292f !important;
background-color: #f6f8fa !important;
}
.markdown-body:not(.dark-theme) pre {
background-color: #f6f8fa !important;
}
.markdown-body:not(.dark-theme) a {
color: #0969da !important;
}
`;
// 移除旧的样式标签(如果存在)
const oldStyle = document.getElementById('theme-styles');
if (oldStyle) {
oldStyle.remove();
}
// 添加新的样式标签(无论是深色还是浅色模式都添加)
const styleTag = document.createElement('style');
styleTag.id = 'theme-styles';
styleTag.textContent = themeStyles;
document.head.appendChild(styleTag);
// 如果是空白状态,更新默认内容的样式
if (!this.properties.url) {
this.inner.innerHTML = `
<div style="
padding: 20px;
background: ${theme.contentBg};
border-radius: 4px;
height: 100%;
box-sizing: border-box;
color: ${theme.textColor};
">
<h3 style="margin: 0 0 10px 0; color: ${theme.headingColor};">Media Player</h3>
<div>
支持:<br>
1. 网页 URL<br>
2. 视频嵌入代码<br>
3. GitHub Markdown地址
</div>
</div>
`;
}
}
}
async updateContent() {
if (!this.inner) return;
let url = this.properties.url;
if (url && url.trim()) {
url = url.trim();
// 处理 GitHub markdown URL
if (url.includes('github.com') && (url.endsWith('.md') || url.includes('/blob/'))) {
try {
// 转换为 raw URL
const rawUrl = url
.replace('github.com', 'raw.githubusercontent.com')
.replace('/blob/', '/');
console.log('获取 GitHub markdown:', rawUrl);
const response = await fetch(rawUrl);
if (!response.ok) {
throw new Error('无法获取 Markdown 内容');
}
let content = await response.text();
// 处理图片路径
const baseUrl = rawUrl.substring(0, rawUrl.lastIndexOf('/'));
content = content.replace(
/!\[([^\]]*)\]\((?!http)([^)]+)\)/g,
(match, alt, path) => {
const imagePath = path.startsWith('./') ? path.slice(2) : path;
return `![${alt}](${baseUrl}/${imagePath})`;
}
);
const isDark = this.properties.theme === 'dark';
this.inner.className = `markdown-body ${isDark ? 'dark-theme' : ''}`;
this.inner.style.cssText = `
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
box-sizing: border-box;
background: ${isDark ? '#0d1117' : 'white'};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
color: ${isDark ? '#E0E0E0' : '#24292f'};
`;
const styleElement = document.createElement('style');
styleElement.textContent = `
.markdown-body img {
max-width: 100% !important;
height: auto !important;
}
.markdown-body pre {
max-width: 100% !important;
overflow-x: auto !important;
}
.markdown-body table {
display: block !important;
max-width: 100% !important;
overflow-x: auto !important;
}
.markdown-body * {
max-width: 100% !important;
box-sizing: border-box !important;
}
`;
this.inner.appendChild(styleElement);
// 转换并显示 markdown
this.inner.innerHTML = marked.parse(content) + styleElement.outerHTML;
return;
} catch (error) {
console.error('处理 Markdown 失败:', error);
this.inner.innerHTML = `<div class="error" style="color: ${this.properties.theme === 'dark' ? '#ff6b6b' : '#ff0000'}">
加载 Markdown 失败: ${error.message}</div>`;
return;
}
}
if (url.includes('<iframe') || url.includes('<video')) {
// 嵌入代码只处理 http 到 https 的改
let secureContent = url.replace(/http:\/\//g, 'https://');
secureContent = secureContent.replace(/\/\/player\.bilibili\.com/g, 'https://player.bilibili.com');
this.inner.innerHTML = `
<div style="
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: ${this.properties.theme === 'dark' ? '#2D2D2D' : 'white'};
border-radius: 4px;
">
${secureContent}
</div>
`;
const elements = this.inner.querySelectorAll('iframe, video');
elements.forEach(element => {
if (element.src && element.src.startsWith('http:')) {
element.src = element.src.replace('http:', 'https:');
}
element.style.cssText = `
width: 100%;
height: 100%;
border: none;
border-radius: 4px;
background: black;
max-width: 100%;
max-height: 100%;
`;
});
} else {
// 只对普通 URL 进行补全
if (!url.match(/^https?:\/\//)) {
url = 'https://' + url;
}
let secureUrl = url;
if (url.startsWith('http:')) {
secureUrl = url.replace('http:', 'https:');
}
this.inner.innerHTML = `
<iframe
src="${secureUrl}"
style="
width: 100%;
height: 100%;
border: none;
border-radius: 4px;
background: ${this.properties.theme === 'dark' ? '#2D2D2D' : 'white'};
"
allowfullscreen
referrerpolicy="no-referrer"
sandbox="allow-scripts allow-same-origin allow-popups allow-presentation"
></iframe>
`;
}
} else {
this.applyTheme();
}
const iframes = this.inner.querySelectorAll('iframe');
iframes.forEach(iframe => {
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
iframe.setAttribute('referrerpolicy', 'no-referrer');
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups allow-presentation');
});
}
onPropertyChanged(name, value) {
super.onPropertyChanged?.(name, value);
if (name === "theme") {
this.properties[name] = value;
// 当主题改变时,确保先应用主题再更新内容
this.applyTheme();
this.updateContent();
}
if (name === "url") {
this.properties[name] = value;
this.updateContent();
}
}
getExtraMenuOptions() {
// 返回全新的菜单数组,不使用或扩展现有选项
return [
{
content: this.properties.theme === 'light' ? "✓ 浅色主题" : "浅色主题",
callback: () => {
this.setProperty("theme", "light");
}
},
{
content: this.properties.theme === 'dark' ? "✓ 深色主题" : "深色主题",
callback: () => {
this.setProperty("theme", "dark");
}
}
];
}
configure(info) {
// 先调用父类的 configure
super.configure(info);
// 确保在配置加载后重新应用主题和更新内容
requestAnimationFrame(() => {
this.applyTheme();
this.updateContent();
});
return this;
}
}
MediaPlayerNode.collapsable = false;
app.registerExtension({
name: "MediaPlayer",
registerCustomNodes() {
MediaPlayerNode.setUp();
}
});