Files

58 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The guided tour: a walk along the screen, one callout per part, on whatever project is open (it changes nothing).
// Help ▸ Start tour / Restart tour start it, Exit tour ends it, Skip to FAQ jumps to the last callout.
import { composer as c } from "../../composer/composer.js";
// Each step points at a part of the screen by name: the menu bar, a zone, the page body, or a button by its label.
const STEPS = [
{ at: "menubar", side: "bottom", title: "Everything starts here", text: "The menu bar above has File, Edit, View and Help. Settings, next to Help, holds the models, the engine and the updates." },
{ at: "zone:0", side: "right", title: "Assets", text: "Your projects and the media bin. Drop images, clips and audio here; right-click a tile to use it as a reference, rename or delete it." },
{ at: "zone:1", side: "left", title: "Preview", text: "The monitor plays the cut wherever the playhead is. Save frame puts the current picture in the media bin." },
{ at: "zone:2", side: "left", title: "Properties", text: "Project settings (size, length, postfix) and the picked scene's prompt, source, references and length." },
{ at: "zone:3", side: "top", title: "Timeline", text: "One clip per scene. Drag the edges to trim, press S to split, Space to play. Lanes below carry separated sound." },
{ at: "button:▶ Generate|Generating…", side: "top", title: "Generate", text: "Makes every scene on the timeline; earlier renders stay as takes. Beside it: Selected, Takes ×N (new seeds) and Rate scene… (teaches FunUI your taste)." },
{ at: "button:⏱ Sampler", side: "top", title: "Sampler", text: "Steps, sampler, scheduler, seed and the common switches, one click from the timeline. While a run goes, the step count shows to its left." },
{ at: "button:◆ Composer", side: "top", title: "Composer", text: "Write the whole story in one box, cut at the cut word; keep templates, $variables and shortcuts; chat with the prompt enhancer." },
{ at: "button:+ Add", side: "top", title: "Add and edit", text: "+ Add opens one menu: a scene, a clip, an effect, a transition or an overlay. Next to it: Split, Remove, Export and more." },
{ at: "button:⧉ Render|Rendering…", side: "top", title: "Render", text: "Joins the whole timeline into one file, with the separated sound mixed in. Export downloads the picked clips." },
{ at: "main", side: "bottom", title: "FAQ", text: "Nothing happens? Check the ComfyUI chip at the top right and the Log. Lost a layout? View ▸ Reset Layout. That is the tour; Help ▸ Start tour runs it again." },
];
export default {
id: "tour",
mount: "menubar.menus",
needs: ["project", "dom"],
setup({ app }) {
let at = -1, pop = null, target = null, watcher = null, queued = false;
// The part pointed at can be redrawn (Generate becomes "Generating..."): the callout then follows the new node instead of a detached one.
const follow = () => {
if (queued || !pop || !target || (target.isConnected && target.getBoundingClientRect().width)) return; // gone from the page, or hidden
queued = true;
requestAnimationFrame(() => { queued = false; if (pop && at >= 0 && target && !(target.isConnected && target.getBoundingClientRect().width)) show(at); });
};
const stop = () => { if (watcher) { watcher.disconnect(); watcher = null; } target = null; if (pop) { const p = pop; pop = null; p.close(); } at = -1; app.has.delete("tour.running"); }; // a flag the Help menu reads: features see copies of app, the Set is shared
const shown = (i) => { const el = app.dom.landmark(STEPS[i].at); return el && el.getBoundingClientRect().width ? el : null; };
function show(i) {
if (pop) { const p = pop; pop = null; p.close(); }
target = null;
while (i >= 0 && i < STEPS.length && !(target = shown(i))) i += i >= at ? 1 : -1; // a part that is hidden right now is skipped
if (i < 0 || i >= STEPS.length) return stop();
at = i;
app.has.add("tour.running");
if (!watcher && typeof MutationObserver === "function") { watcher = new MutationObserver(follow); watcher.observe(app.dom.body, { childList: true, subtree: true }); watcher.observe(app.dom.body.ownerDocument.documentElement, { attributes: true, attributeFilter: ["class"] }); }
const seen = STEPS.map((_, k) => k).filter((k) => k === i || shown(k)), place = seen.indexOf(i) + 1; // numbered among the parts on screen, so the count has no gaps
const s = STEPS[i], last = i === STEPS.length - 1;
const body = c.region.stack({ gap: "sm", children: [c.label.section({ text: `${place} / ${seen.length} · ${s.title}` }), c.text.sm({ text: s.text }),
c.toolbar.default({ items: [c.button.sm({ label: "Back", tone: "ghost", disabled: i === 0, onClick: () => show(i - 1) }), c.button.sm({ label: last ? "Done" : "Next", tone: "primary", onClick: () => (last ? stop() : show(i + 1)) })],
trailing: [c.button.sm({ label: "Exit tour", tone: "ghost", onClick: stop })] })] });
pop = c.popover.anchored({ anchor: target, body, side: s.side, align: "center", gap: 10, closeOnOutside: false, onClose: () => { if (pop) { pop = null; at = -1; if (watcher) { watcher.disconnect(); watcher = null; } app.has.delete("tour.running"); } } });
}
app.has.add("tour");
const off = app.on((what) => {
if (what === "tour.start") show(0);
else if (what === "tour.faq") show(STEPS.length - 1);
else if (what === "tour.stop") stop();
});
return () => { off(); stop(); app.has.delete("tour"); app.has.delete("tour.running"); };
},
};