Files

248 lines
9.7 KiB
JavaScript

import { t } from "../i18n.js";
import { API } from "../api.js";
import { formatDate } from "../utils.js";
import { showDialog, showConfirm } from "./Dialog.js";
import { showContextMenu } from "./ContextMenu.js";
export class WorkflowItem {
constructor(wf, groups, opts = {}) {
this.wf = wf;
this.groups = groups;
this.opts = opts;
}
async _pickCustomThumbnail() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.style.display = "none";
document.body.appendChild(input);
input.addEventListener("change", async () => {
const file = input.files?.[0];
input.remove();
if (!file) return;
const dataUrl = await new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 180;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(0, 0, 320, 180);
const scale = Math.min(320 / img.width, 180 / img.height);
const width = img.width * scale;
const height = img.height * scale;
ctx.drawImage(img, (320 - width) / 2, (180 - height) / 2, width, height);
resolve(canvas.toDataURL("image/png"));
};
img.onerror = reject;
img.src = url;
});
const res = await API.saveThumbnail(this.wf.id, dataUrl);
if (res.status === "success") this.opts.onRefresh?.();
});
input.click();
}
render() {
const { wf, groups } = this;
const el = document.createElement("div");
el.draggable = true;
el.style.cssText = `
display:flex;align-items:center;gap:10px;
padding:9px 10px;background:#222;
border:1px solid #2e2e2e;border-radius:7px;
margin-bottom:5px;cursor:default;
transition:background .12s, border-color .12s;
`;
el.addEventListener("mouseenter", () => {
el.style.background = "#2a2a2a";
el.style.borderColor = "#3a3a3a";
});
el.addEventListener("mouseleave", () => {
el.style.background = "#222";
el.style.borderColor = "#2e2e2e";
});
el.addEventListener("dragstart", event => {
event.dataTransfer.setData("workflowId", wf.id);
event.dataTransfer.effectAllowed = "move";
el.style.opacity = "0.45";
});
el.addEventListener("dragend", () => { el.style.opacity = "1"; });
const thumb = document.createElement("div");
thumb.style.cssText = `
width:54px;height:36px;flex-shrink:0;border-radius:4px;
background:#1a1a1a;border:1px solid #333;overflow:hidden;
display:flex;align-items:center;justify-content:center;
`;
if (wf.has_thumbnail) {
const img = document.createElement("img");
img.src = API.thumbnailUrl(wf.id);
img.style.cssText = "width:100%;height:100%;object-fit:cover;";
img.onerror = () => { thumb.innerHTML = `<span style="font-size:18px;color:#333">🖼️</span>`; };
thumb.appendChild(img);
} else {
thumb.innerHTML = `<span style="font-size:18px;color:#333">🖼️</span>`;
}
const info = document.createElement("div");
info.style.cssText = "flex:1;overflow:hidden;cursor:pointer;min-width:0;";
const groupName = wf.group ? (groups[wf.group]?.name || wf.group) : "";
info.innerHTML = `
<div style="
color:#ddd;font-size:13px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
">${wf.name}</div>
<div style="color:#555;font-size:11px;margin-top:3px;display:flex;gap:6px;flex-wrap:wrap;">
<span>${formatDate(wf.modified_at)}</span>
${groupName ? `<span style="color:#444">· ${groupName}</span>` : ""}
${wf.tags?.length ? `<span style="color:#0066cc">${wf.tags.map(tag => `#${tag}`).join(" ")}</span>` : ""}
</div>
`;
info.addEventListener("click", () => this.opts.onLoad?.(wf.id));
const starBtn = document.createElement("button");
starBtn.textContent = wf.starred ? "★" : "☆";
starBtn.title = wf.starred ? t("unstar") : t("starred");
starBtn.style.cssText = `
background:none;border:none;padding:0;flex-shrink:0;
color:${wf.starred ? "#ffd700" : "#444"};
cursor:pointer;font-size:15px;line-height:1;
transition:color .12s;
`;
starBtn.addEventListener("mouseenter", () => { if (!wf.starred) starBtn.style.color = "#888"; });
starBtn.addEventListener("mouseleave", () => { if (!wf.starred) starBtn.style.color = "#444"; });
starBtn.addEventListener("click", async event => {
event.stopPropagation();
await API.toggleStar(wf.id);
this.opts.onRefresh?.();
});
const moreBtn = document.createElement("button");
moreBtn.textContent = "⋯";
moreBtn.style.cssText = `
background:none;border:none;padding:0 2px;flex-shrink:0;
color:#555;cursor:pointer;font-size:17px;line-height:1;
transition:color .12s;
`;
moreBtn.addEventListener("mouseenter", () => moreBtn.style.color = "#aaa");
moreBtn.addEventListener("mouseleave", () => moreBtn.style.color = "#555");
moreBtn.addEventListener("click", event => {
event.stopPropagation();
this._showMenu(event);
});
el.appendChild(thumb);
el.appendChild(info);
el.appendChild(starBtn);
el.appendChild(moreBtn);
return el;
}
_showMenu(event) {
const { wf } = this;
showContextMenu(event, [
{
icon: "📂",
label: t("loadWorkflow"),
action: () => this.opts.onLoad?.(wf.id),
},
{
icon: "🖼️",
label: t("customCover"),
action: () => this._pickCustomThumbnail(),
},
"divider",
{
icon: "✏️",
label: t("rename"),
action: async () => {
const result = await showDialog({
title: t("renameWorkflow"),
fields: [{ key: "name", label: t("newName"), default: wf.name }],
});
if (!result?.name || result.name === wf.name) return;
await API.rename(wf.id, result.name);
this.opts.onRefresh?.();
},
},
{
icon: "🏷️",
label: t("editTags"),
action: async () => {
const result = await showDialog({
title: t("editTags"),
fields: [{
key: "tags",
label: t("tagsComma"),
default: wf.tags?.join(", ") || "",
placeholder: "tag1, tag2, tag3",
}],
});
if (result === null) return;
const tags = result.tags.split(",").map(tag => tag.trim()).filter(Boolean);
await API.updateTags(wf.id, tags);
this.opts.onRefresh?.();
},
},
{
icon: "📋",
label: t("copyTo"),
action: async () => {
const result = await showDialog({
title: t("copyWorkflow"),
fields: [{ key: "name", label: t("newName"), default: `${wf.name}_copy` }],
});
if (!result?.name) return;
await API.copy(wf.id, wf.group || null, result.name);
this.opts.onRefresh?.();
},
},
{
icon: "🕐",
label: t("versionHistory"),
action: () => this.opts.onShowVersions?.(wf.id, wf.name),
},
{
icon: "💾",
label: t("backup"),
action: async () => {
const res = await API.backup(wf.id);
if (res.status === "success") {
await showConfirm({
title: t("backupSuccess"),
message: `${t("savedTo")}\n${res.backup_path}`,
danger: false,
});
}
},
},
"divider",
{
icon: "🗑️",
label: t("delete"),
danger: true,
action: async () => {
const ok = await showConfirm({
title: t("deleteWorkflow"),
message: t("deleteWorkflowConfirm", { name: wf.name }),
danger: true,
});
if (!ok) return;
await API.delete(wf.id);
this.opts.onRefresh?.();
},
},
]);
}
}