Add files via upload

This commit is contained in:
hanL34
2025-10-15 10:07:52 +08:00
committed by GitHub
parent b68855b895
commit 74d6e21d2e
12 changed files with 2980 additions and 0 deletions
+69
View File
@@ -196,6 +196,75 @@ async def get_preview_image(request):
except Exception as e:
return web.Response(status=404)
@PromptServer.instance.routes.get("/power_artist_loader/csv/read")
async def read_csv(request):
"""读取CSV文件内容API"""
try:
csv_path = os.path.join(os.path.dirname(__file__), "artists.csv")
artists = []
if os.path.exists(csv_path):
async with aiofiles.open(csv_path, 'r', encoding='utf-8') as f:
content = await f.read()
for line in content.split('\n'):
line = line.strip()
if not line:
continue
parts = line.split(';')
if len(parts) >= 2:
artists.append({
'name': parts[0].strip(),
'keywords': parts[1].strip(),
'image': parts[2].strip() if len(parts) > 2 else ''
})
return web.json_response({
'success': True,
'artists': artists
})
except Exception as e:
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
@PromptServer.instance.routes.post("/power_artist_loader/csv/save")
async def save_csv(request):
"""保存CSV文件API"""
try:
data = await request.json()
artists = data.get('artists', [])
csv_path = os.path.join(os.path.dirname(__file__), "artists.csv")
# 构建CSV内容
lines = []
for artist in artists:
name = artist.get('name', '').strip()
keywords = artist.get('keywords', '').strip()
image = artist.get('image', '').strip()
if name: # 只保存有名字的行
lines.append(f"{name};{keywords};{image}")
# 写入文件
async with aiofiles.open(csv_path, 'w', encoding='utf-8') as f:
await f.write('\n'.join(lines))
# 重新加载数据到内存
power_artist_loader_instance.load_artists_data()
return web.json_response({
'success': True,
'message': f'Saved {len(lines)} artists'
})
except Exception as e:
return web.json_response({
'success': False,
'error': str(e)
}, status=500)
# 节点类映射
NODE_CLASS_MAPPINGS = {
"PowerArtistLoader": PowerArtistLoader
+1878
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "comfyui-power-artist-loader-manager",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"watch": "vite build --watch"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@comfyorg/comfyui-frontend-types": "^1.20.2",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.4.2",
"vite": "^5.2.10"
}
}
+184
View File
@@ -0,0 +1,184 @@
import React, { useState, useEffect, useRef } from 'react';
interface Artist {
name: string;
keywords: string;
image: string;
}
interface ArtistManagerProps {
api: any;
onClose?: () => void;
}
const ArtistManager: React.FC<ArtistManagerProps> = ({ api, onClose }) => {
const [artists, setArtists] = useState<Artist[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
// 加载CSV数据
const loadArtists = async () => {
try {
setLoading(true);
const response = await api.fetchApi('/power_artist_loader/csv/read');
const data = await response.json();
if (data.success) {
setArtists(data.artists);
setMessage('');
} else {
setMessage('Failed to load artists: ' + data.error);
}
} catch (error) {
setMessage('Error loading artists: ' + error);
console.error('Load error:', error);
} finally {
setLoading(false);
}
};
// 保存CSV数据
const saveArtists = async () => {
try {
setSaving(true);
const response = await api.fetchApi('/power_artist_loader/csv/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ artists })
});
const data = await response.json();
if (data.success) {
setMessage('✓ Saved successfully!');
setTimeout(() => setMessage(''), 3000);
} else {
setMessage('Failed to save: ' + data.error);
}
} catch (error) {
setMessage('Error saving: ' + error);
console.error('Save error:', error);
} finally {
setSaving(false);
}
};
// 添加新画师
const addArtist = () => {
setArtists([...artists, { name: '', keywords: '', image: '' }]);
};
// 删除画师
const deleteArtist = (index: number) => {
setArtists(artists.filter((_, i) => i !== index));
};
// 更新画师信息
const updateArtist = (index: number, field: keyof Artist, value: string) => {
const newArtists = [...artists];
newArtists[index][field] = value;
setArtists(newArtists);
};
useEffect(() => {
loadArtists();
}, []);
if (loading) {
return <div className="artist-manager-loading">Loading artists...</div>;
}
return (
<div className="artist-manager">
<div className="artist-manager-header">
<h2>🎨 Artist Library Manager</h2>
<div className="artist-manager-actions">
<button onClick={addArtist} className="btn btn-add">
+ Add Artist
</button>
<button onClick={saveArtists} disabled={saving} className="btn btn-save">
{saving ? 'Saving...' : '💾 Save'}
</button>
<button onClick={loadArtists} className="btn btn-refresh">
🔄 Refresh
</button>
{onClose && (
<button onClick={onClose} className="btn btn-close">
✕ Close
</button>
)}
</div>
</div>
{message && (
<div className={`artist-manager-message ${message.includes('✓') ? 'success' : 'error'}`}>
{message}
</div>
)}
<div className="artist-table-container">
<table className="artist-table">
<thead>
<tr>
<th className="col-name">Artist Name</th>
<th className="col-keywords">Keywords</th>
<th className="col-actions">Action</th>
</tr>
</thead>
<tbody>
{artists.map((artist, index) => (
<tr key={index}>
<td className="col-name">
<div className="artist-name-cell">
<span className="artist-index">{index + 1}</span>
<input
type="text"
value={artist.name}
onChange={(e) => updateArtist(index, 'name', e.target.value)}
placeholder="Artist name"
className="input-field"
/>
</div>
<input
type="text"
value={artist.image}
onChange={(e) => updateArtist(index, 'image', e.target.value)}
placeholder="image.jpg"
className="input-field input-image"
/>
</td>
<td className="col-keywords">
<textarea
value={artist.keywords}
onChange={(e) => updateArtist(index, 'keywords', e.target.value)}
placeholder="Keywords"
className="textarea-field"
/>
</td>
<td className="col-actions">
<button
onClick={() => deleteArtist(index)}
className="btn btn-delete"
title="Delete"
>
🗑️
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="artist-manager-footer">
<p className="artist-count">📊 Total: <strong>{artists.length}</strong> artists</p>
<p className="help-text">
Format: name;keywords;image.jpg | Save updates artists.csv
</p>
</div>
</div>
);
};
export default ArtistManager;
+82
View File
@@ -0,0 +1,82 @@
import { app } from '/scripts/app.js';
import { api } from '/scripts/api.js';
import React from 'react';
import { createRoot } from 'react-dom/client';
import ArtistManager from './ArtistManager';
import './styles.css';
// 等待 ComfyUI 应用初始化
app.registerExtension({
name: "PowerArtistLoader.Manager",
async setup() {
try {
// 添加右键菜单选项
const getCanvasMenuOptions = app.canvas.getCanvasMenuOptions;
app.canvas.getCanvasMenuOptions = function () {
const options = getCanvasMenuOptions.apply(this, arguments);
options.push({
content: "🎨 Artist Manager",
callback: () => {
openArtistManager();
}
});
return options;
};
console.log("Power Artist Manager menu registered");
} catch (error) {
console.error("Failed to register Artist Manager:", error);
}
}
});
// 打开管理器窗口
function openArtistManager() {
// 创建模态窗口
const modal = document.createElement('div');
modal.className = 'artist-manager-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
`;
const container = document.createElement('div');
container.style.cssText = `
width: 90%;
height: 85%;
max-width: 1200px;
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`;
modal.appendChild(container);
document.body.appendChild(modal);
// 渲染React组件
const root = createRoot(container);
root.render(<ArtistManager api={api} onClose={() => {
root.unmount();
document.body.removeChild(modal);
}} />);
// 点击背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
root.unmount();
document.body.removeChild(modal);
}
});
}
+345
View File
@@ -0,0 +1,345 @@
/* 主容器 */
.artist-manager {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
background: #1e1e1e;
color: #e0e0e0;
overflow: hidden;
}
/* 头部区域 */
.artist-manager-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #333;
}
.artist-manager-header h2 {
margin: 0;
font-size: 20px;
color: #4caf50;
}
.artist-manager-actions {
display: flex;
gap: 8px;
}
/* 按钮通用样式 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-family: inherit;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-add {
background: #4caf50;
color: #fff;
}
.btn-add:hover:not(:disabled) {
background: #45a049;
}
.btn-save {
background: #2196f3;
color: #fff;
}
.btn-save:hover:not(:disabled) {
background: #1976d2;
}
.btn-refresh {
background: #ff9800;
color: #fff;
}
.btn-refresh:hover:not(:disabled) {
background: #f57c00;
}
.btn-close {
background: #757575;
color: #fff;
}
.btn-close:hover:not(:disabled) {
background: #616161;
}
.btn-delete {
background: #f44336;
color: #fff;
padding: 6px 12px;
font-size: 16px;
min-width: 40px;
}
.btn-delete:hover:not(:disabled) {
background: #d32f2f;
}
/* 消息提示 */
.artist-manager-message {
padding: 12px;
margin-bottom: 12px;
border-radius: 4px;
font-size: 14px;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.artist-manager-message.success {
background: #4caf50;
color: #fff;
}
.artist-manager-message.error {
background: #f44336;
color: #fff;
}
/* 加载状态 */
.artist-manager-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 18px;
color: #4caf50;
}
/* 表格容器 */
.artist-table-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
border: 1px solid #333;
border-radius: 4px;
margin-bottom: 12px;
background: #1a1a1a;
}
/* 表格 */
.artist-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
table-layout: fixed;
}
.artist-table thead {
position: sticky;
top: 0;
background: #2d2d2d;
z-index: 10;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.artist-table th {
padding: 12px 4px;
text-align: left;
font-weight: 600;
color: #4caf50;
border-bottom: 2px solid #4caf50;
}
.artist-table td {
padding: 8px 4px;
border-bottom: 1px solid #333;
vertical-align: top;
}
.artist-table tbody tr {
transition: background 0.15s ease;
}
.artist-table tbody tr:hover {
background: #252525;
}
/* 列宽定义 */
.col-name {
width: 25%;
}
.col-keywords {
width: 65%;
}
.col-actions {
width: 10%;
text-align: center;
}
/* 艺术家名称单元格 */
.artist-name-cell {
display: flex;
flex-direction: column;
gap: 6px;
align-items: stretch;
}
.artist-index {
display: inline-block;
background: #333;
color: #4caf50;
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
align-self: flex-start;
}
/* 输入框样式 */
.input-field {
width: 100%;
padding: 8px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 3px;
color: #e0e0e0;
font-size: 13px;
box-sizing: border-box;
font-family: inherit;
transition: all 0.2s ease;
}
.input-field:focus {
outline: none;
border-color: #4caf50;
background: #333;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);
}
.input-field::placeholder {
color: #666;
}
.input-image {
font-size: 12px;
color: #999;
padding: 6px 8px;
}
.input-image::placeholder {
color: #555;
}
/* 文本域样式 */
.textarea-field {
width: 100%;
min-height: 80px;
max-height: 200px;
padding: 8px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 3px;
color: #e0e0e0;
font-size: 13px;
font-family: inherit;
resize: vertical;
box-sizing: border-box;
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.5;
transition: all 0.2s ease;
}
.textarea-field:focus {
outline: none;
border-color: #4caf50;
background: #333;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);
}
.textarea-field::placeholder {
color: #666;
}
/* 底部区域 */
.artist-manager-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #333;
font-size: 13px;
color: #999;
flex-shrink: 0;
}
.artist-count {
margin: 0;
font-weight: 500;
}
.artist-count strong {
color: #4caf50;
font-size: 14px;
}
.help-text {
font-style: italic;
margin: 0;
color: #777;
}
/* 滚动条样式 */
.artist-table-container::-webkit-scrollbar {
width: 10px;
}
.artist-table-container::-webkit-scrollbar-track {
background: #1a1a1a;
border-radius: 5px;
}
.artist-table-container::-webkit-scrollbar-thumb {
background: #4caf50;
border-radius: 5px;
border: 2px solid #1a1a1a;
}
.artist-table-container::-webkit-scrollbar-thumb:hover {
background: #45a049;
}
/* Firefox 滚动条 */
.artist-table-container {
scrollbar-width: thin;
scrollbar-color: #4caf50 #1a1a1a;
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.js"]
}
+41
View File
@@ -0,0 +1,41 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
const rewriteComfyImports = ({ isDev }) => {
return {
name: "rewrite-comfy-imports",
resolveId(source) {
if (!isDev) return;
if (source === "/scripts/app.js") {
return "http://127.0.0.1:8188/scripts/app.js";
}
if (source === "/scripts/api.js") {
return "http://127.0.0.1:8188/scripts/api.js";
}
return null;
},
};
};
export default defineConfig(({ mode }) => ({
plugins: [
react(),
rewriteComfyImports({ isDev: mode === "development" })
],
build: {
emptyOutDir: true,
rollupOptions: {
external: ['/scripts/app.js', '/scripts/api.js'],
input: {
main: path.resolve(__dirname, 'src/main.tsx'),
},
output: {
dir: '../web/dist',
entryFileNames: 'artist-manager.js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name][extname]',
}
}
}
}));
+59
View File
File diff suppressed because one or more lines are too long
+268
View File
@@ -0,0 +1,268 @@
.artist-manager {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 16px;
background: #1e1e1e;
color: #e0e0e0;
overflow: hidden;
}
.artist-manager-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #333;
}
.artist-manager-header h2 {
margin: 0;
font-size: 20px;
color: #4caf50;
}
.artist-manager-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-add {
background: #4caf50;
color: #fff;
}
.btn-add:hover {
background: #45a049;
}
.btn-save {
background: #2196f3;
color: #fff;
}
.btn-save:hover {
background: #1976d2;
}
.btn-refresh {
background: #ff9800;
color: #fff;
}
.btn-refresh:hover {
background: #f57c00;
}
.btn-close {
background: #757575;
color: #fff;
}
.btn-close:hover {
background: #616161;
}
.btn-delete {
background: #f44336;
color: #fff;
padding: 4px 8px;
font-size: 16px;
}
.btn-delete:hover {
background: #d32f2f;
}
.artist-manager-message {
padding: 12px;
margin-bottom: 12px;
border-radius: 4px;
font-size: 14px;
}
.artist-manager-message.success {
background: #4caf50;
color: #fff;
}
.artist-manager-message.error {
background: #f44336;
color: #fff;
}
.artist-manager-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 18px;
color: #4caf50;
}
.artist-table-container {
flex: 1;
overflow-y: auto;
border: 1px solid #333;
border-radius: 4px;
margin-bottom: 12px;
}
.artist-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
table-layout: fixed;
}
.artist-table thead {
position: sticky;
top: 0;
background: #2d2d2d;
z-index: 10;
}
.artist-table th {
padding: 12px 8px;
text-align: left;
font-weight: 600;
color: #4caf50;
border-bottom: 2px solid #4caf50;
}
.artist-table td {
padding: 8px;
border-bottom: 1px solid #333;
vertical-align: top;
}
.artist-table tbody tr:hover {
background: #252525;
}
.col-name {
width: 25%;
padding-left: 4px !important;
padding-right: 4px !important;
}
.col-keywords {
width: 65%;
padding-left: 4px !important;
padding-right: 4px !important;
}
.col-actions {
width: 10%;
text-align: center;
padding-left: 4px !important;
padding-right: 4px !important;
}
.artist-name-cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.input-field {
width: 100%;
padding: 6px 8px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 3px;
color: #e0e0e0;
font-size: 13px;
box-sizing: border-box;
}
.input-field:focus {
outline: none;
border-color: #4caf50;
background: #333;
}
.input-image {
margin-top: 4px;
font-size: 12px;
color: #999;
}
.textarea-field {
width: 100%;
min-height: 60px;
padding: 6px 8px;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 3px;
color: #e0e0e0;
font-size: 13px;
font-family: inherit;
resize: vertical;
box-sizing: border-box;
white-space: pre-wrap;
word-wrap: break-word;
}
.textarea-field:focus {
outline: none;
border-color: #4caf50;
background: #333;
}
.artist-manager-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid #333;
font-size: 13px;
color: #999;
}
.artist-count {
margin: 0;
}
.help-text {
font-style: italic;
margin: 0;
}
.artist-table-container::-webkit-scrollbar {
width: 8px;
}
.artist-table-container::-webkit-scrollbar-track {
background: #1e1e1e;
}
.artist-table-container::-webkit-scrollbar-thumb {
background: #4caf50;
border-radius: 4px;
}
.artist-table-container::-webkit-scrollbar-thumb:hover {
background: #45a049;
}