feat: add general video generation workflow
This commit is contained in:
397
public/app.js
397
public/app.js
@@ -15,6 +15,8 @@ const LEGACY_GROUP_KEY = "yingxiangli.selectedAssetGroup";
|
||||
const SCENE_ROLES_KEY = "yingxiangli.sceneRoles.v1";
|
||||
const CHARACTER_LIBRARY_KEY = "yingxiangli.characterLibrary.v1";
|
||||
const DRAMA_PROJECT_KEY = "yingxiangli.dramaProject.v1";
|
||||
const GENERAL_VIDEO_STATE_KEY = "yingxiangli.generalVideo.v1";
|
||||
const PENDING_GENERAL_VIDEO_KEY = "yingxiangli.pendingGeneralVideo.v1";
|
||||
const VIDEO_KEY_STORAGE = "yingxiangli.videoKey.v1";
|
||||
const AUDIT_CLIENT_KEY = "yingxiangli.auditClientId.v1";
|
||||
const AUDIT_SESSION_KEY = "yingxiangli.auditSessionId.v1";
|
||||
@@ -41,6 +43,9 @@ const state = {
|
||||
activeShotId: null,
|
||||
activeGenerationShotId: null,
|
||||
activeGenerationVersionId: null,
|
||||
generalSelectedReferences: new Set(),
|
||||
generalResults: [],
|
||||
generalPollTimer: null,
|
||||
};
|
||||
|
||||
const el = Object.fromEntries([
|
||||
@@ -59,6 +64,10 @@ const el = Object.fromEntries([
|
||||
"videoKeyGate", "videoKeyForm", "videoKeyInput", "videoKeyError", "changeVideoKeyButton", "cancelVideoKeyButton",
|
||||
"genericAssetForm", "genericAssetType", "genericAssetUrl", "genericAssetPurpose", "genericAssetSubmit", "genericAssetStatus",
|
||||
"genericAssetHint", "genericAssetResult", "genericAssetResultText", "genericAssetList", "genericAssetHistoryCount", "refreshGenericAssets",
|
||||
"generalVideoForm", "generalVideoStatus", "generalPrompt", "generalAssetPicker", "refreshGeneralAssets",
|
||||
"generalImageInputs", "generalVideoInputs", "addGeneralImageButton", "addGeneralVideoButton", "generalInputQuota",
|
||||
"generalModel", "generalRatio", "generalDuration", "generalResolution", "generalGenerateAudio", "generalWatermark",
|
||||
"generalGenerateButton", "generalResultCount", "generalResultList",
|
||||
].map((id) => [id, document.getElementById(id)]));
|
||||
|
||||
const reviewRows = [el.reviewFront, el.reviewSide, el.reviewBack];
|
||||
@@ -83,6 +92,7 @@ async function init() {
|
||||
restoreSceneRoles();
|
||||
restoreCharacterLibrary();
|
||||
restoreDramaProject();
|
||||
restoreGeneralVideoState();
|
||||
renderConnection();
|
||||
if (!state.config.mockMode && !currentVideoKey()) {
|
||||
showVideoKeyGate(false);
|
||||
@@ -95,6 +105,7 @@ async function init() {
|
||||
resumePendingReview();
|
||||
resumePendingGenericAsset();
|
||||
if (state.config.mockMode || currentVideoKey()) resumePendingVideo();
|
||||
if (state.config.mockMode || currentVideoKey()) resumePendingGeneralVideo();
|
||||
} catch (error) {
|
||||
showToast(error.message, true);
|
||||
el.connectionState.textContent = "本地服务连接失败";
|
||||
@@ -204,6 +215,16 @@ el.assetForm.addEventListener("submit", async (event) => {
|
||||
|
||||
el.genericAssetForm.addEventListener("submit", submitGenericAsset);
|
||||
el.genericAssetType.addEventListener("change", updateGenericAssetForm);
|
||||
el.generalVideoForm.addEventListener("submit", submitGeneralVideo);
|
||||
el.refreshGeneralAssets.addEventListener("click", loadAssets);
|
||||
el.addGeneralImageButton.addEventListener("click", () => addGeneralMediaInput("image"));
|
||||
el.addGeneralVideoButton.addEventListener("click", () => addGeneralMediaInput("video"));
|
||||
[el.generalImageInputs, el.generalVideoInputs].forEach((list) => list.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-remove-general-media]");
|
||||
if (button) removeGeneralMediaInput(button);
|
||||
}));
|
||||
[el.generalPrompt, el.generalImageInputs, el.generalVideoInputs].forEach((input) => input.addEventListener("input", saveGeneralVideoState));
|
||||
[el.generalModel, el.generalRatio, el.generalDuration, el.generalResolution, el.generalGenerateAudio, el.generalWatermark].forEach((input) => input.addEventListener("change", saveGeneralVideoState));
|
||||
|
||||
el.videoForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -327,6 +348,7 @@ async function loadAssets() {
|
||||
renderCharacterLibrary();
|
||||
renderAssetPicker();
|
||||
renderGenericAssetLibrary();
|
||||
renderGeneralAssetPicker();
|
||||
el.assetHistoryCount.textContent = String(state.assets.length);
|
||||
el.assetList.innerHTML = "";
|
||||
if (!state.assets.length) {
|
||||
@@ -446,6 +468,379 @@ function renderGenericAssetLibrary() {
|
||||
}
|
||||
}
|
||||
|
||||
function restoreGeneralVideoState() {
|
||||
let saved;
|
||||
try { saved = JSON.parse(localStorage.getItem(GENERAL_VIDEO_STATE_KEY) || "null"); } catch { saved = null; }
|
||||
state.generalSelectedReferences = new Set(Array.isArray(saved?.selectedReferences) ? saved.selectedReferences.map(String) : []);
|
||||
state.generalResults = Array.isArray(saved?.results) ? saved.results.slice(0, 20) : [];
|
||||
el.generalPrompt.value = String(saved?.prompt || "");
|
||||
el.generalModel.value = saved?.model || "doubao-seedance-2-0-260128";
|
||||
el.generalRatio.value = saved?.ratio || "9:16";
|
||||
el.generalDuration.value = String(Math.min(14, Math.max(5, Number(saved?.duration) || 5)));
|
||||
el.generalResolution.value = saved?.resolution === "480p" ? "480p" : "720p";
|
||||
el.generalGenerateAudio.checked = saved?.generateAudio !== false;
|
||||
el.generalWatermark.checked = Boolean(saved?.watermark);
|
||||
renderGeneralMediaInputs("image", saved?.imageUrls || "");
|
||||
renderGeneralMediaInputs("video", saved?.videoUrls || "");
|
||||
renderGeneralResults();
|
||||
updateGeneralQuota();
|
||||
}
|
||||
|
||||
function saveGeneralVideoState() {
|
||||
localStorage.setItem(GENERAL_VIDEO_STATE_KEY, JSON.stringify({
|
||||
prompt: el.generalPrompt.value,
|
||||
selectedReferences: [...state.generalSelectedReferences],
|
||||
imageUrls: collectGeneralMediaUrls("image").join("\n"),
|
||||
videoUrls: collectGeneralMediaUrls("video").join("\n"),
|
||||
model: el.generalModel.value,
|
||||
ratio: el.generalRatio.value,
|
||||
duration: Number(el.generalDuration.value),
|
||||
resolution: el.generalResolution.value,
|
||||
generateAudio: el.generalGenerateAudio.checked,
|
||||
watermark: el.generalWatermark.checked,
|
||||
results: state.generalResults.slice(0, 20),
|
||||
}));
|
||||
updateGeneralQuota();
|
||||
}
|
||||
|
||||
function renderGeneralAssetPicker() {
|
||||
if (!el.generalAssetPicker) return;
|
||||
const available = new Set(state.genericAssets.map(normalizeAssetReference));
|
||||
for (const reference of [...state.generalSelectedReferences]) {
|
||||
if (!available.has(reference)) state.generalSelectedReferences.delete(reference);
|
||||
}
|
||||
el.generalAssetPicker.innerHTML = "";
|
||||
if (!state.genericAssets.length) {
|
||||
el.generalAssetPicker.innerHTML = '<div class="empty-small">通用素材库暂无可用图片或视频</div>';
|
||||
updateGeneralQuota();
|
||||
return;
|
||||
}
|
||||
for (const item of state.genericAssets) {
|
||||
const type = assetItemType(item);
|
||||
const reference = normalizeAssetReference(item);
|
||||
const sourceUrl = item.original_url || item.URL || "";
|
||||
const name = genericAssetName(item, sourceUrl);
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `general-asset-choice${state.generalSelectedReferences.has(reference) ? " selected" : ""}`;
|
||||
const preview = document.createElement("span");
|
||||
preview.className = "general-asset-choice-preview";
|
||||
if (sourceUrl && type === "image") {
|
||||
const image = document.createElement("img");
|
||||
image.src = sourceUrl;
|
||||
image.alt = name;
|
||||
image.loading = "lazy";
|
||||
preview.appendChild(image);
|
||||
} else if (sourceUrl && type === "video") {
|
||||
const video = document.createElement("video");
|
||||
video.src = sourceUrl;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.preload = "metadata";
|
||||
preview.appendChild(video);
|
||||
} else preview.textContent = type === "video" ? "视频" : "图片";
|
||||
const copy = document.createElement("span");
|
||||
copy.className = "general-asset-choice-copy";
|
||||
copy.innerHTML = `<strong>${escapeHtml(name)}</strong><span>${type === "video" ? "视频" : "图片"}</span>`;
|
||||
button.append(preview, copy);
|
||||
button.addEventListener("click", () => toggleGeneralAsset(reference, type));
|
||||
el.generalAssetPicker.appendChild(button);
|
||||
}
|
||||
updateGeneralQuota();
|
||||
}
|
||||
|
||||
function toggleGeneralAsset(reference, type) {
|
||||
if (state.generalSelectedReferences.has(reference)) state.generalSelectedReferences.delete(reference);
|
||||
else {
|
||||
const quota = generalMediaQuota(reference, type);
|
||||
if (!quota.valid) return showToast(quota.message, true);
|
||||
state.generalSelectedReferences.add(reference);
|
||||
}
|
||||
saveGeneralVideoState();
|
||||
renderGeneralAssetPicker();
|
||||
}
|
||||
|
||||
function generalMediaInputConfig(type) {
|
||||
return type === "video"
|
||||
? { list: el.generalVideoInputs, button: el.addGeneralVideoButton, limit: 3, placeholder: "公网 MP4/MOV 或 asset:// 素材引用" }
|
||||
: { list: el.generalImageInputs, button: el.addGeneralImageButton, limit: 9, placeholder: "公网图片 URL 或 asset:// 素材引用" };
|
||||
}
|
||||
|
||||
function createGeneralMediaInput(type, value = "") {
|
||||
const { placeholder } = generalMediaInputConfig(type);
|
||||
const row = document.createElement("div");
|
||||
row.className = "media-url-row";
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.value = value;
|
||||
input.placeholder = placeholder;
|
||||
input.setAttribute("aria-label", type === "video" ? "通用生成参考视频" : "通用生成参考图片");
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "media-remove-button";
|
||||
remove.dataset.removeGeneralMedia = type;
|
||||
remove.setAttribute("aria-label", "删除该链接");
|
||||
remove.textContent = "×";
|
||||
row.append(input, remove);
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderGeneralMediaInputs(type, value) {
|
||||
const { list } = generalMediaInputConfig(type);
|
||||
const values = parseMediaLines(value);
|
||||
list.replaceChildren(...(values.length ? values : [""]).map((item) => createGeneralMediaInput(type, item)));
|
||||
refreshGeneralMediaButton(type);
|
||||
}
|
||||
|
||||
function collectGeneralMediaUrls(type) {
|
||||
const { list } = generalMediaInputConfig(type);
|
||||
return [...new Set([...list.querySelectorAll("input")].map((input) => input.value.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
function addGeneralMediaInput(type) {
|
||||
const { list, limit } = generalMediaInputConfig(type);
|
||||
if (list.children.length >= limit) return;
|
||||
const row = createGeneralMediaInput(type);
|
||||
list.appendChild(row);
|
||||
row.querySelector("input").focus();
|
||||
refreshGeneralMediaButton(type);
|
||||
}
|
||||
|
||||
function removeGeneralMediaInput(button) {
|
||||
const type = button.dataset.removeGeneralMedia;
|
||||
const { list } = generalMediaInputConfig(type);
|
||||
if (list.children.length === 1) list.querySelector("input").value = "";
|
||||
else button.closest(".media-url-row").remove();
|
||||
refreshGeneralMediaButton(type);
|
||||
saveGeneralVideoState();
|
||||
}
|
||||
|
||||
function refreshGeneralMediaButton(type) {
|
||||
const { list, button, limit } = generalMediaInputConfig(type);
|
||||
button.disabled = list.children.length >= limit;
|
||||
}
|
||||
|
||||
function selectedGeneralLibraryMedia() {
|
||||
const selected = state.genericAssets.filter((item) => state.generalSelectedReferences.has(normalizeAssetReference(item)));
|
||||
return {
|
||||
images: selected.filter((item) => assetItemType(item) === "image").map(normalizeAssetReference),
|
||||
videos: selected.filter((item) => assetItemType(item) === "video").map(normalizeAssetReference),
|
||||
};
|
||||
}
|
||||
|
||||
function generalMediaQuota(pendingReference = "", pendingType = "") {
|
||||
const selected = selectedGeneralLibraryMedia();
|
||||
const imageReferences = new Set([...selected.images, ...collectGeneralMediaUrls("image")]);
|
||||
const videoReferences = new Set([...selected.videos, ...collectGeneralMediaUrls("video")]);
|
||||
if (pendingReference && !state.generalSelectedReferences.has(pendingReference)) {
|
||||
if (pendingType === "video") videoReferences.add(pendingReference);
|
||||
else imageReferences.add(pendingReference);
|
||||
}
|
||||
const imageCount = imageReferences.size;
|
||||
const videoCount = videoReferences.size;
|
||||
if (imageCount > 9) return { valid: false, imageCount, videoCount, message: `参考图片 ${imageCount}/9,已超出上限` };
|
||||
if (videoCount > 3) return { valid: false, imageCount, videoCount, message: `参考视频 ${videoCount}/3,已超出上限` };
|
||||
return { valid: true, imageCount, videoCount, message: "可提交" };
|
||||
}
|
||||
|
||||
function updateGeneralQuota() {
|
||||
if (!el.generalInputQuota) return;
|
||||
const quota = generalMediaQuota();
|
||||
el.generalInputQuota.textContent = `参考图片 ${quota.imageCount}/9 · 参考视频 ${quota.videoCount}/3`;
|
||||
el.generalInputQuota.className = `input-quota wide ${quota.valid ? "valid" : "invalid"}`;
|
||||
}
|
||||
|
||||
async function submitGeneralVideo(event) {
|
||||
event.preventDefault();
|
||||
const prompt = el.generalPrompt.value.trim();
|
||||
if (!prompt) return showToast("请填写视频提示词", true);
|
||||
const quota = generalMediaQuota();
|
||||
if (!quota.valid) return showToast(quota.message, true);
|
||||
const selected = selectedGeneralLibraryMedia();
|
||||
const images = [...new Set([...selected.images, ...collectGeneralMediaUrls("image")])];
|
||||
const videos = [...new Set([...selected.videos, ...collectGeneralMediaUrls("video")])];
|
||||
const result = {
|
||||
id: `general-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
status: "running",
|
||||
taskId: "",
|
||||
videoUrl: "",
|
||||
error: "",
|
||||
ratio: el.generalRatio.value,
|
||||
createdAt: Date.now(),
|
||||
lastCheckedAt: null,
|
||||
progress: 5,
|
||||
};
|
||||
state.generalResults.unshift(result);
|
||||
state.generalResults = state.generalResults.slice(0, 20);
|
||||
el.generalGenerateButton.disabled = true;
|
||||
setGeneralVideoStatus("processing", "生成中");
|
||||
saveGeneralVideoState();
|
||||
renderGeneralResults();
|
||||
try {
|
||||
const response = await api("/api/videos", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
assetReferences: [],
|
||||
extraImageReferences: images,
|
||||
videoReferences: videos,
|
||||
prompt,
|
||||
model: el.generalModel.value,
|
||||
ratio: el.generalRatio.value,
|
||||
duration: Number(el.generalDuration.value),
|
||||
resolution: el.generalResolution.value,
|
||||
generateAudio: el.generalGenerateAudio.checked,
|
||||
watermark: el.generalWatermark.checked,
|
||||
}),
|
||||
});
|
||||
const taskId = response.task_id || response.id || response.data?.task_id || response.data?.id;
|
||||
if (!taskId) throw new Error("视频接口未返回任务 ID");
|
||||
result.taskId = taskId;
|
||||
result.mock = Boolean(response.mock);
|
||||
localStorage.setItem(PENDING_GENERAL_VIDEO_KEY, JSON.stringify({ taskId, resultId: result.id, startedAt: result.createdAt }));
|
||||
saveGeneralVideoState();
|
||||
renderGeneralResults();
|
||||
await pollGeneralVideoTask(taskId, result.id);
|
||||
} catch (error) {
|
||||
const message = contextualizeGenerationError(error.message);
|
||||
if (error.pending) {
|
||||
setGeneralVideoStatus("processing", "仍在生成");
|
||||
showToast(message);
|
||||
} else {
|
||||
result.status = "failed";
|
||||
result.error = message;
|
||||
localStorage.removeItem(PENDING_GENERAL_VIDEO_KEY);
|
||||
setGeneralVideoStatus("error", "生成失败");
|
||||
showToast(message, true);
|
||||
}
|
||||
} finally {
|
||||
saveGeneralVideoState();
|
||||
renderGeneralResults();
|
||||
el.generalGenerateButton.disabled = state.generalResults.some((item) => item.status === "running");
|
||||
}
|
||||
}
|
||||
|
||||
async function pollGeneralVideoTask(taskId, resultId) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < VIDEO_POLL_MAX_MS) {
|
||||
await delay(VIDEO_POLL_INTERVAL_MS);
|
||||
const response = await api(`/api/videos/${encodeURIComponent(taskId)}`);
|
||||
const status = response._normalized?.status || normalizeVideoStatus(response.status || response.data?.status);
|
||||
const result = state.generalResults.find((item) => item.id === resultId);
|
||||
if (!result) return;
|
||||
result.taskId = taskId;
|
||||
result.lastCheckedAt = Date.now();
|
||||
result.progress = Math.max(10, Number(response.progress ?? response.data?.progress ?? (status === "completed" ? 100 : 20)));
|
||||
result.mock = result.mock || Boolean(response.mock);
|
||||
saveGeneralVideoState();
|
||||
renderGeneralResults();
|
||||
if (status === "completed") {
|
||||
result.status = "completed";
|
||||
result.progress = 100;
|
||||
result.videoUrl = response._normalized?.videoUrl || response.video_url || response.output?.video_url || response.data?.video_url || "";
|
||||
localStorage.removeItem(PENDING_GENERAL_VIDEO_KEY);
|
||||
setGeneralVideoStatus("success", "生成完成");
|
||||
saveGeneralVideoState();
|
||||
renderGeneralResults();
|
||||
showToast("通用视频已生成");
|
||||
return;
|
||||
}
|
||||
if (status === "failed") throw new Error(response.error?.message || response.message || "视频生成失败");
|
||||
}
|
||||
const error = new Error("视频仍在服务商侧生成,任务已保存;稍后重新打开页面会继续查询");
|
||||
error.pending = true;
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function resumePendingGeneralVideo() {
|
||||
let pending;
|
||||
try { pending = JSON.parse(localStorage.getItem(PENDING_GENERAL_VIDEO_KEY) || "null"); } catch { pending = null; }
|
||||
if (!pending?.taskId) return;
|
||||
switchModule("general-video");
|
||||
let result = state.generalResults.find((item) => item.id === pending.resultId);
|
||||
if (!result) {
|
||||
result = { id: pending.resultId || `general-${Date.now()}`, status: "running", taskId: pending.taskId, videoUrl: "", error: "", ratio: "9:16", createdAt: pending.startedAt || Date.now(), lastCheckedAt: null, progress: 20 };
|
||||
state.generalResults.unshift(result);
|
||||
}
|
||||
result.status = "running";
|
||||
el.generalGenerateButton.disabled = true;
|
||||
setGeneralVideoStatus("processing", "继续查询");
|
||||
renderGeneralResults();
|
||||
try {
|
||||
await pollGeneralVideoTask(pending.taskId, result.id);
|
||||
} catch (error) {
|
||||
if (error.pending) {
|
||||
setGeneralVideoStatus("processing", "仍在生成");
|
||||
showToast(error.message);
|
||||
} else {
|
||||
result.status = "failed";
|
||||
result.error = contextualizeGenerationError(error.message);
|
||||
localStorage.removeItem(PENDING_GENERAL_VIDEO_KEY);
|
||||
setGeneralVideoStatus("error", "生成失败");
|
||||
showToast(result.error, true);
|
||||
}
|
||||
} finally {
|
||||
saveGeneralVideoState();
|
||||
renderGeneralResults();
|
||||
el.generalGenerateButton.disabled = state.generalResults.some((item) => item.status === "running");
|
||||
}
|
||||
}
|
||||
|
||||
function renderGeneralResults() {
|
||||
if (!el.generalResultList) return;
|
||||
el.generalResultCount.textContent = state.generalResults.length ? `${state.generalResults.length} 条记录` : "暂无记录";
|
||||
el.generalResultList.innerHTML = "";
|
||||
if (!state.generalResults.length) {
|
||||
el.generalResultList.innerHTML = '<div class="empty-small">提交任务后,生成状态和视频会出现在这里</div>';
|
||||
return;
|
||||
}
|
||||
state.generalResults.forEach((result, index) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "version-card";
|
||||
const preview = document.createElement("div");
|
||||
preview.className = "version-preview";
|
||||
setVersionPreviewRatio(preview, result.ratio || "16:9");
|
||||
if (result.videoUrl) {
|
||||
const video = document.createElement("video");
|
||||
video.src = result.videoUrl;
|
||||
video.controls = true;
|
||||
video.playsInline = true;
|
||||
video.preload = "metadata";
|
||||
video.addEventListener("loadedmetadata", () => {
|
||||
if (video.videoWidth && video.videoHeight) setVersionPreviewRatio(preview, `${video.videoWidth}:${video.videoHeight}`);
|
||||
}, { once: true });
|
||||
preview.appendChild(video);
|
||||
} else preview.textContent = result.status === "running" ? `${result.progress || 20}% · 生成中` : result.status === "completed" ? "流程已完成" : "生成失败";
|
||||
const copy = document.createElement("div");
|
||||
copy.className = "version-copy";
|
||||
const statusText = result.status === "completed" ? "已完成" : result.status === "running" ? `生成中 · 已等待 ${formatElapsed(result.createdAt)}` : "失败";
|
||||
copy.innerHTML = `<strong>视频 ${state.generalResults.length - index} · ${statusText}</strong><span>${escapeHtml(result.taskId || result.error || "正在获取任务 ID")}</span>`;
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "version-actions";
|
||||
if (result.status === "completed" && result.taskId && !result.mock) {
|
||||
const download = document.createElement("a");
|
||||
download.className = "video-action-link";
|
||||
download.href = result.videoUrl || videoProxyUrl(result.taskId);
|
||||
download.textContent = "下载视频";
|
||||
download.setAttribute("download", "");
|
||||
download.target = "_blank";
|
||||
download.rel = "noopener";
|
||||
const copyLink = document.createElement("button");
|
||||
copyLink.type = "button";
|
||||
copyLink.className = "video-action-link";
|
||||
copyLink.textContent = "复制视频链接";
|
||||
copyLink.addEventListener("click", () => copyGeneratedVideoLink(result.taskId));
|
||||
actions.append(download, copyLink);
|
||||
}
|
||||
card.append(preview, copy, actions);
|
||||
el.generalResultList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function setGeneralVideoStatus(kind, text) {
|
||||
el.generalVideoStatus.className = `status-badge ${kind}`;
|
||||
el.generalVideoStatus.textContent = text;
|
||||
}
|
||||
|
||||
function useGenericAssetInActiveShot(reference, name, type) {
|
||||
const shot = activeShot();
|
||||
if (type === "video") {
|
||||
@@ -629,7 +1024,7 @@ function bindCharacterToActiveRole(character) {
|
||||
}
|
||||
|
||||
function switchModule(module) {
|
||||
if (!["library", "generic-library", "production"].includes(module)) return;
|
||||
if (!["library", "generic-library", "general-video", "production"].includes(module)) return;
|
||||
state.activeModule = module;
|
||||
document.body.dataset.activeModule = module;
|
||||
document.querySelectorAll('.module-tab').forEach((button) => {
|
||||
|
||||
Reference in New Issue
Block a user