Files
awesome-copilot/extensions/draw/public/app.js
T

139 lines
4.5 KiB
JavaScript

// Boots the Draw canvas: theme, editor, input, UI and sync with the extension.
import { Editor } from "./editor.js";
import { LabelEditor } from "./labels.js";
import { attachPointer } from "./pointer.js";
import { attachKeys } from "./keys.js";
import { UI } from "./ui.js";
import { attachMenus } from "./menus.js";
import { Sync } from "./sync.js";
import { blobToBase64 } from "./util.js";
import { readTheme, applyThemeVars, watchTheme, setMeasureFont, clearMeasureCache, measure, THEME_MODES } from "./theme.js";
const $ = (id) => document.getElementById(id);
const params = new URLSearchParams(location.search);
const instanceId = params.get("i") || "";
const token = params.get("t") || "";
function loadTheme(mode) {
const t = readTheme(mode);
setMeasureFont(t.font);
applyThemeVars($("app"), t);
return { ...t, measure };
}
// The page stays invisible until the host's theme is in, or briefly when there is no host.
const reveal = () => $("app").classList.remove("booting");
async function boot() {
// The server writes the saved theme choice into the page, so the first paint is already right.
let mode = THEME_MODES.includes($("app").dataset.themeMode) ? $("app").dataset.themeMode : "app";
let theme = loadTheme(mode);
if (theme.present.length) reveal();
else setTimeout(reveal, 400);
const ed = new Editor(
{ stage: $("stage"), svg: $("svg"), world: $("world"), content: $("content"), overlay: $("overlay"), layer: $("layer") },
theme,
);
new LabelEditor(ed);
attachPointer(ed);
const ui = new UI(ed);
ui.themeMode = () => mode;
ui.setThemeMode = (next) => applyMode(next, true);
attachMenus(ui);
ui.syncThemeButton();
attachKeys(ed, ui);
// Match app, light or dark. Saving the choice also switches every other open panel.
function applyMode(next, save) {
if (!THEME_MODES.includes(next)) return;
if (next !== mode) {
mode = next;
$("app").dataset.themeMode = mode;
theme = loadTheme(mode);
ed.setTheme(theme);
ui.refresh();
ui.syncThemeButton();
}
if (save) {
sync.post("settings", { theme: next }).catch((err) => ui.toast(`Could not save the theme: ${err.message}`, { error: true }));
}
}
// Draws the picture an agent asked for. The reply names the drawing this panel shows, so the
// server can turn it down when that is not the one it asked for.
async function answerExport({ requestId, format, drawingId: wanted }) {
const drawingId = sync.drawingId;
const reply = (fields) => sync.post("export", { requestId, drawingId, format, ...fields });
try {
if (wanted !== drawingId) return await reply({});
await reply({ data: format === "svg" ? ed.exportSVG() : await blobToBase64(await ed.exportPNG()) });
} catch (err) {
reply({ error: err.message || String(err) }).catch(() => {});
}
}
const sync = new Sync({
instanceId,
token,
editor: ed,
handlers: {
connection: (ok) => ui.setConnection(ok),
list: (drawings) => ui.setDrawings(drawings),
switched: (drawing) => {
ui.closePopover();
ui.cancelRename?.();
ed.load(drawing);
ui.setDoc(drawing);
ui.syncEmpty();
},
exportRequest: answerExport,
select: (ids) => ed.select(ids),
status: (kind, msg) => ui.setStatus(kind, msg),
problem: (msg) => ui.toast(msg, { error: true, timeout: 8000 }),
unsaved: (drawing) => ui.warnUnsaved(() => ui.drawingsAction({ action: "open", id: drawing.id }, { discard: true })),
settings: (values) => applyMode(values.theme, false),
},
});
ui.sync = sync;
ed.on("selection", () => sync.sendSelection([...ed.selection]));
watchTheme(() => {
theme = loadTheme(mode);
ed.setTheme(theme);
ui.refresh();
if (theme.present.length) reveal();
});
document.fonts?.ready.then(() => {
clearMeasureCache();
ed.refresh();
});
if (!instanceId || !token) {
reveal();
ui.fatal("This page must be opened from the Draw canvas in Copilot.");
return;
}
let state;
try {
state = await sync.loadState();
} catch (err) {
reveal();
ui.fatal(err.message);
return;
}
if (state.settings) applyMode(state.settings.theme, false);
sync.setDoc(state.drawing);
ui.setup(state);
ed.load(state.drawing);
ui.syncEmpty();
sync.setDiskError(state.drawing.saveError);
sync.connect();
ed.focusCanvas();
}
boot().catch((err) => {
reveal();
const stage = $("stage");
if (stage) stage.textContent = `Draw failed to start: ${err.message}`;
});