The duplicate functions at lines 455-486 were redefining cacheElements and bindEvents with wrong element IDs (kebab-case vs camelCase in HTML). This caused 'Cannot read properties of null' error on slides app init.
2895 lines
85 KiB
JavaScript
2895 lines
85 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
const CONFIG = {
|
||
CANVAS_WIDTH: 960,
|
||
CANVAS_HEIGHT: 540,
|
||
MAX_HISTORY: 50,
|
||
AUTOSAVE_DELAY: 3000,
|
||
WS_RECONNECT_DELAY: 5000,
|
||
MIN_ELEMENT_SIZE: 20,
|
||
};
|
||
|
||
const state = {
|
||
presentationId: null,
|
||
presentationName: "Untitled Presentation",
|
||
slides: [],
|
||
currentSlideIndex: 0,
|
||
selectedElement: null,
|
||
clipboard: null,
|
||
history: [],
|
||
historyIndex: -1,
|
||
zoom: 100,
|
||
collaborators: [],
|
||
ws: null,
|
||
isDragging: false,
|
||
isResizing: false,
|
||
isRotating: false,
|
||
dragStart: null,
|
||
resizeHandle: null,
|
||
isDirty: false,
|
||
autoSaveTimer: null,
|
||
isPresenting: false,
|
||
theme: null,
|
||
driveSource: null,
|
||
chatPanelOpen: true,
|
||
};
|
||
|
||
const elements = {};
|
||
|
||
function init() {
|
||
cacheElements();
|
||
bindEvents();
|
||
createNewPresentation();
|
||
loadFromUrlParams();
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
}
|
||
|
||
function cacheElements() {
|
||
elements.app = document.getElementById("slides-app");
|
||
elements.presentationName = document.getElementById("presentationName");
|
||
elements.thumbnailsPanel = document.getElementById("thumbnailsPanel");
|
||
elements.thumbnails = document.getElementById("thumbnails");
|
||
elements.canvasContainer = document.getElementById("canvasContainer");
|
||
elements.slideCanvas = document.getElementById("slideCanvas");
|
||
elements.canvasContent = document.getElementById("canvasContent");
|
||
elements.selectionHandles = document.getElementById("selectionHandles");
|
||
elements.cursorIndicators = document.getElementById("cursorIndicators");
|
||
elements.collaborators = document.getElementById("collaborators");
|
||
elements.slideInfo = document.getElementById("slideInfo");
|
||
elements.saveStatus = document.getElementById("saveStatus");
|
||
elements.zoomLevel = document.getElementById("zoomLevel");
|
||
elements.chatPanel = document.getElementById("chatPanel");
|
||
elements.chatMessages = document.getElementById("chatMessages");
|
||
elements.chatInput = document.getElementById("chatInput");
|
||
elements.chatForm = document.getElementById("chatForm");
|
||
elements.contextMenu = document.getElementById("contextMenu");
|
||
elements.slideContextMenu = document.getElementById("slideContextMenu");
|
||
elements.presenterModal = document.getElementById("presenterModal");
|
||
}
|
||
|
||
function bindEvents() {
|
||
if (elements.presentationName) {
|
||
elements.presentationName.addEventListener("change", (e) => {
|
||
state.presentationName = e.target.value || "Untitled Presentation";
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
});
|
||
}
|
||
|
||
document.getElementById("undoBtn")?.addEventListener("click", undo);
|
||
document.getElementById("redoBtn")?.addEventListener("click", redo);
|
||
|
||
document
|
||
.getElementById("addTextBtn")
|
||
?.addEventListener("click", addTextBox);
|
||
document
|
||
.getElementById("addImageBtn")
|
||
?.addEventListener("click", () => showModal("imageModal"));
|
||
document
|
||
.getElementById("addShapeBtn")
|
||
?.addEventListener("click", () => showModal("shapeModal"));
|
||
document.getElementById("addTableBtn")?.addEventListener("click", addTable);
|
||
document
|
||
.getElementById("addSlideBtn")
|
||
?.addEventListener("click", () => addSlide());
|
||
|
||
document.getElementById("boldBtn")?.addEventListener("click", toggleBold);
|
||
document
|
||
.getElementById("italicBtn")
|
||
?.addEventListener("click", toggleItalic);
|
||
document
|
||
.getElementById("underlineBtn")
|
||
?.addEventListener("click", toggleUnderline);
|
||
|
||
document
|
||
.getElementById("fontFamily")
|
||
?.addEventListener("change", (e) => setFontFamily(e.target.value));
|
||
document
|
||
.getElementById("fontSize")
|
||
?.addEventListener("change", (e) => setFontSize(e.target.value));
|
||
|
||
document.getElementById("textColorBtn")?.addEventListener("click", () => {
|
||
document.getElementById("textColorPicker")?.click();
|
||
});
|
||
document
|
||
.getElementById("textColorPicker")
|
||
?.addEventListener("input", (e) => setTextColor(e.target.value));
|
||
document.getElementById("fillColorBtn")?.addEventListener("click", () => {
|
||
document.getElementById("fillColorPicker")?.click();
|
||
});
|
||
document
|
||
.getElementById("fillColorPicker")
|
||
?.addEventListener("input", (e) => setFillColor(e.target.value));
|
||
|
||
document
|
||
.getElementById("alignLeftBtn")
|
||
?.addEventListener("click", () => setTextAlign("left"));
|
||
document
|
||
.getElementById("alignCenterBtn")
|
||
?.addEventListener("click", () => setTextAlign("center"));
|
||
document
|
||
.getElementById("alignRightBtn")
|
||
?.addEventListener("click", () => setTextAlign("right"));
|
||
|
||
document
|
||
.getElementById("presentBtn")
|
||
?.addEventListener("click", startPresentation);
|
||
document
|
||
.getElementById("shareBtn")
|
||
?.addEventListener("click", () => showModal("shareModal"));
|
||
|
||
document
|
||
.getElementById("transitionsBtn")
|
||
?.addEventListener("click", showTransitionsModal);
|
||
document
|
||
.getElementById("closeTransitionsModal")
|
||
?.addEventListener("click", () => hideModal("transitionsModal"));
|
||
document
|
||
.getElementById("applyTransitionsBtn")
|
||
?.addEventListener("click", applyTransition);
|
||
document
|
||
.getElementById("cancelTransitionsBtn")
|
||
?.addEventListener("click", () => hideModal("transitionsModal"));
|
||
document
|
||
.getElementById("transitionDuration")
|
||
?.addEventListener("input", updateDurationDisplay);
|
||
document.querySelectorAll(".transition-btn").forEach((btn) => {
|
||
btn.addEventListener("click", () =>
|
||
selectTransition(btn.dataset.transition),
|
||
);
|
||
});
|
||
|
||
document
|
||
.getElementById("animationsBtn")
|
||
?.addEventListener("click", showAnimationsModal);
|
||
document
|
||
.getElementById("closeAnimationsModal")
|
||
?.addEventListener("click", () => hideModal("animationsModal"));
|
||
document
|
||
.getElementById("applyAnimationsBtn")
|
||
?.addEventListener("click", applyAnimation);
|
||
document
|
||
.getElementById("cancelAnimationsBtn")
|
||
?.addEventListener("click", () => hideModal("animationsModal"));
|
||
document
|
||
.getElementById("previewAnimationBtn")
|
||
?.addEventListener("click", previewAnimation);
|
||
|
||
document
|
||
.getElementById("slideSorterBtn")
|
||
?.addEventListener("click", showSlideSorter);
|
||
document
|
||
.getElementById("closeSlideSorterModal")
|
||
?.addEventListener("click", () => hideModal("slideSorterModal"));
|
||
document
|
||
.getElementById("applySorterBtn")
|
||
?.addEventListener("click", applySorterChanges);
|
||
document
|
||
.getElementById("cancelSorterBtn")
|
||
?.addEventListener("click", () => hideModal("slideSorterModal"));
|
||
document
|
||
.getElementById("sorterAddSlide")
|
||
?.addEventListener("click", sorterAddSlide);
|
||
document
|
||
.getElementById("sorterDuplicateSlide")
|
||
?.addEventListener("click", sorterDuplicateSlide);
|
||
document
|
||
.getElementById("sorterDeleteSlide")
|
||
?.addEventListener("click", sorterDeleteSlide);
|
||
|
||
document
|
||
.getElementById("masterSlideBtn")
|
||
?.addEventListener("click", showMasterSlideModal);
|
||
document
|
||
.getElementById("closeMasterSlideModal")
|
||
?.addEventListener("click", () => hideModal("masterSlideModal"));
|
||
document
|
||
.getElementById("applyMasterBtn")
|
||
?.addEventListener("click", applyMasterSlide);
|
||
document
|
||
.getElementById("cancelMasterBtn")
|
||
?.addEventListener("click", () => hideModal("masterSlideModal"));
|
||
document
|
||
.getElementById("resetMasterBtn")
|
||
?.addEventListener("click", resetMasterSlide);
|
||
document.querySelectorAll(".master-layout-item").forEach((item) => {
|
||
item.addEventListener("click", () =>
|
||
selectMasterLayout(item.dataset.layout),
|
||
);
|
||
});
|
||
document
|
||
.getElementById("masterPrimaryColor")
|
||
?.addEventListener("input", updateMasterPreview);
|
||
document
|
||
.getElementById("masterSecondaryColor")
|
||
?.addEventListener("input", updateMasterPreview);
|
||
document
|
||
.getElementById("masterAccentColor")
|
||
?.addEventListener("input", updateMasterPreview);
|
||
document
|
||
.getElementById("masterBgColor")
|
||
?.addEventListener("input", updateMasterPreview);
|
||
document
|
||
.getElementById("masterTextColor")
|
||
?.addEventListener("input", updateMasterPreview);
|
||
document
|
||
.getElementById("masterTextLightColor")
|
||
?.addEventListener("input", updateMasterPreview);
|
||
document
|
||
.getElementById("masterHeadingFont")
|
||
?.addEventListener("change", updateMasterPreview);
|
||
document
|
||
.getElementById("masterBodyFont")
|
||
?.addEventListener("change", updateMasterPreview);
|
||
|
||
document
|
||
.getElementById("exportPdfBtn")
|
||
?.addEventListener("click", showExportPdfModal);
|
||
document
|
||
.getElementById("closeExportPdfModal")
|
||
?.addEventListener("click", () => hideModal("exportPdfModal"));
|
||
document
|
||
.getElementById("exportPdfBtnConfirm")
|
||
?.addEventListener("click", exportToPdf);
|
||
document
|
||
.getElementById("cancelExportPdfBtn")
|
||
?.addEventListener("click", () => hideModal("exportPdfModal"));
|
||
|
||
document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn);
|
||
document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut);
|
||
|
||
document
|
||
.getElementById("chatToggle")
|
||
?.addEventListener("click", toggleChatPanel);
|
||
document
|
||
.getElementById("chatClose")
|
||
?.addEventListener("click", toggleChatPanel);
|
||
elements.chatForm?.addEventListener("submit", handleChatSubmit);
|
||
|
||
document.querySelectorAll(".suggestion-btn").forEach((btn) => {
|
||
btn.addEventListener("click", () =>
|
||
handleSuggestionClick(btn.dataset.action),
|
||
);
|
||
});
|
||
|
||
document.querySelectorAll(".btn-close").forEach((btn) => {
|
||
btn.addEventListener("click", (e) => {
|
||
const modal = e.target.closest(".modal");
|
||
if (modal) modal.classList.add("hidden");
|
||
});
|
||
});
|
||
|
||
document
|
||
.getElementById("closeShareModal")
|
||
?.addEventListener("click", () => hideModal("shareModal"));
|
||
document
|
||
.getElementById("closeImageModal")
|
||
?.addEventListener("click", () => hideModal("imageModal"));
|
||
document
|
||
.getElementById("closeShapeModal")
|
||
?.addEventListener("click", () => hideModal("shapeModal"));
|
||
document
|
||
.getElementById("closeNotesModal")
|
||
?.addEventListener("click", () => hideModal("notesModal"));
|
||
document
|
||
.getElementById("closeBackgroundModal")
|
||
?.addEventListener("click", () => hideModal("backgroundModal"));
|
||
|
||
document
|
||
.getElementById("insertImageBtn")
|
||
?.addEventListener("click", insertImage);
|
||
document
|
||
.getElementById("saveNotesBtn")
|
||
?.addEventListener("click", saveNotes);
|
||
document
|
||
.getElementById("applyBgBtn")
|
||
?.addEventListener("click", applyBackground);
|
||
document
|
||
.getElementById("copyLinkBtn")
|
||
?.addEventListener("click", copyShareLink);
|
||
|
||
document.querySelectorAll(".shape-btn").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
addShape(btn.dataset.shape);
|
||
hideModal("shapeModal");
|
||
});
|
||
});
|
||
|
||
if (elements.canvasContent) {
|
||
elements.canvasContent.addEventListener(
|
||
"mousedown",
|
||
handleCanvasMouseDown,
|
||
);
|
||
elements.canvasContent.addEventListener(
|
||
"dblclick",
|
||
handleCanvasDoubleClick,
|
||
);
|
||
}
|
||
|
||
document.addEventListener("mousemove", handleMouseMove);
|
||
document.addEventListener("mouseup", handleMouseUp);
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
document.addEventListener("contextmenu", handleContextMenu);
|
||
document.addEventListener("click", handleDocumentClick);
|
||
|
||
document.querySelectorAll(".context-item").forEach((item) => {
|
||
item.addEventListener("click", () =>
|
||
handleContextAction(item.dataset.action),
|
||
);
|
||
});
|
||
|
||
document
|
||
.getElementById("prevSlideBtn")
|
||
?.addEventListener("click", () => navigatePresentation(-1));
|
||
document
|
||
.getElementById("nextSlideBtn")
|
||
?.addEventListener("click", () => navigatePresentation(1));
|
||
document
|
||
.getElementById("exitPresenterBtn")
|
||
?.addEventListener("click", exitPresentation);
|
||
|
||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||
}
|
||
|
||
function handleBeforeUnload(e) {
|
||
if (state.isDirty) {
|
||
e.preventDefault();
|
||
e.returnValue = "";
|
||
}
|
||
}
|
||
|
||
async function loadFromUrlParams() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const hash = window.location.hash;
|
||
let presentationId = urlParams.get("id");
|
||
let bucket = urlParams.get("bucket");
|
||
let path = urlParams.get("path");
|
||
|
||
if (hash) {
|
||
const hashQueryIndex = hash.indexOf("?");
|
||
if (hashQueryIndex > -1) {
|
||
const hashParams = new URLSearchParams(hash.slice(hashQueryIndex + 1));
|
||
presentationId = presentationId || hashParams.get("id");
|
||
bucket = bucket || hashParams.get("bucket");
|
||
path = path || hashParams.get("path");
|
||
} else if (hash.startsWith("#id=")) {
|
||
presentationId = hash.slice(4);
|
||
}
|
||
}
|
||
|
||
if (bucket && path) {
|
||
await loadFromDrive(bucket, path);
|
||
} else if (presentationId) {
|
||
try {
|
||
const response = await fetch(`/api/slides/${presentationId}`);
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
state.presentationId = presentationId;
|
||
state.presentationName = data.name || "Untitled Presentation";
|
||
state.slides = data.slides || [];
|
||
|
||
if (elements.presentationName) {
|
||
elements.presentationName.value = state.presentationName;
|
||
}
|
||
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
}
|
||
} catch (e) {
|
||
console.error("Load failed:", e);
|
||
createNewPresentation();
|
||
}
|
||
} else {
|
||
createNewPresentation();
|
||
}
|
||
}
|
||
|
||
async function loadFromDrive(bucket, path) {
|
||
const fileName = path.split("/").pop() || "presentation";
|
||
|
||
state.driveSource = { bucket, path };
|
||
state.presentationName = fileName;
|
||
|
||
if (elements.presentationName) {
|
||
elements.presentationName.value = fileName;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch("/api/files/read", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ bucket, path }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to load file: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
const content = data.content || "";
|
||
|
||
createNewPresentation();
|
||
if (state.slides.length > 0 && state.slides[0].elements) {
|
||
const titleElement = state.slides[0].elements.find(
|
||
(el) => el.element_type === "text" && el.style?.fontSize >= 32,
|
||
);
|
||
if (titleElement) {
|
||
titleElement.content = fileName.replace(/\.[^/.]+$/, "");
|
||
}
|
||
}
|
||
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
state.isDirty = false;
|
||
} catch (err) {
|
||
console.error("Failed to load file from drive:", err);
|
||
createNewPresentation();
|
||
}
|
||
}
|
||
|
||
function createNewPresentation() {
|
||
const titleSlide = createSlide("title");
|
||
state.slides = [titleSlide];
|
||
state.currentSlideIndex = 0;
|
||
state.theme = createDefaultTheme();
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
}
|
||
|
||
function createSlide(layout) {
|
||
const slide = {
|
||
id: generateId(),
|
||
layout: layout,
|
||
elements: [],
|
||
background: {
|
||
bg_type: "solid",
|
||
color: "#ffffff",
|
||
},
|
||
notes: null,
|
||
transition: {
|
||
transition_type: "fade",
|
||
duration: 0.5,
|
||
},
|
||
};
|
||
|
||
switch (layout) {
|
||
case "title":
|
||
slide.elements.push(
|
||
createTextElement(100, 200, 760, 100, "Presentation Title", {
|
||
fontSize: 48,
|
||
fontWeight: "bold",
|
||
textAlign: "center",
|
||
color: "#1e293b",
|
||
}),
|
||
);
|
||
slide.elements.push(
|
||
createTextElement(100, 320, 760, 50, "Subtitle or Author Name", {
|
||
fontSize: 24,
|
||
textAlign: "center",
|
||
color: "#64748b",
|
||
}),
|
||
);
|
||
break;
|
||
case "title-content":
|
||
slide.elements.push(
|
||
createTextElement(50, 40, 860, 60, "Slide Title", {
|
||
fontSize: 36,
|
||
fontWeight: "bold",
|
||
color: "#1e293b",
|
||
}),
|
||
);
|
||
slide.elements.push(
|
||
createTextElement(
|
||
50,
|
||
120,
|
||
860,
|
||
400,
|
||
"• Click to add content\n• Add your bullet points here",
|
||
{
|
||
fontSize: 20,
|
||
color: "#374151",
|
||
lineHeight: 1.6,
|
||
},
|
||
),
|
||
);
|
||
break;
|
||
case "two-column":
|
||
slide.elements.push(
|
||
createTextElement(50, 40, 860, 60, "Slide Title", {
|
||
fontSize: 36,
|
||
fontWeight: "bold",
|
||
color: "#1e293b",
|
||
}),
|
||
);
|
||
slide.elements.push(
|
||
createTextElement(50, 120, 410, 400, "Left column content", {
|
||
fontSize: 18,
|
||
color: "#374151",
|
||
}),
|
||
);
|
||
slide.elements.push(
|
||
createTextElement(500, 120, 410, 400, "Right column content", {
|
||
fontSize: 18,
|
||
color: "#374151",
|
||
}),
|
||
);
|
||
break;
|
||
case "section":
|
||
slide.elements.push(
|
||
createTextElement(100, 220, 760, 100, "Section Title", {
|
||
fontSize: 48,
|
||
fontWeight: "bold",
|
||
textAlign: "center",
|
||
color: "#1e293b",
|
||
}),
|
||
);
|
||
break;
|
||
case "blank":
|
||
default:
|
||
break;
|
||
}
|
||
|
||
return slide;
|
||
}
|
||
|
||
function createTextElement(x, y, width, height, text, style) {
|
||
return {
|
||
id: generateId(),
|
||
element_type: "text",
|
||
x: x,
|
||
y: y,
|
||
width: width,
|
||
height: height,
|
||
rotation: 0,
|
||
content: { text: text },
|
||
style: {
|
||
fontFamily: style.fontFamily || "Inter",
|
||
fontSize: style.fontSize || 16,
|
||
fontWeight: style.fontWeight || "normal",
|
||
fontStyle: style.fontStyle || "normal",
|
||
textAlign: style.textAlign || "left",
|
||
verticalAlign: style.verticalAlign || "top",
|
||
color: style.color || "#000000",
|
||
lineHeight: style.lineHeight || 1.4,
|
||
...style,
|
||
},
|
||
animations: [],
|
||
z_index: 1,
|
||
locked: false,
|
||
};
|
||
}
|
||
|
||
function createShapeElement(x, y, width, height, shapeType, style) {
|
||
return {
|
||
id: generateId(),
|
||
element_type: "shape",
|
||
x: x,
|
||
y: y,
|
||
width: width,
|
||
height: height,
|
||
rotation: 0,
|
||
content: { shape_type: shapeType },
|
||
style: {
|
||
fill: style.fill || "#3b82f6",
|
||
stroke: style.stroke || "none",
|
||
strokeWidth: style.strokeWidth || 0,
|
||
opacity: style.opacity || 1,
|
||
borderRadius: style.borderRadius || 0,
|
||
...style,
|
||
},
|
||
animations: [],
|
||
z_index: 1,
|
||
locked: false,
|
||
};
|
||
}
|
||
|
||
function createImageElement(x, y, width, height, src) {
|
||
return {
|
||
id: generateId(),
|
||
element_type: "image",
|
||
x: x,
|
||
y: y,
|
||
width: width,
|
||
height: height,
|
||
rotation: 0,
|
||
content: { src: src },
|
||
style: {
|
||
opacity: 1,
|
||
borderRadius: 0,
|
||
},
|
||
animations: [],
|
||
z_index: 1,
|
||
locked: false,
|
||
};
|
||
}
|
||
|
||
function createDefaultTheme() {
|
||
return {
|
||
name: "Default",
|
||
colors: {
|
||
primary: "#3b82f6",
|
||
secondary: "#64748b",
|
||
accent: "#f59e0b",
|
||
background: "#ffffff",
|
||
text: "#1e293b",
|
||
text_light: "#64748b",
|
||
},
|
||
fonts: {
|
||
heading: "Inter",
|
||
body: "Inter",
|
||
},
|
||
};
|
||
}
|
||
|
||
function renderThumbnails() {
|
||
if (!elements.thumbnails) return;
|
||
|
||
elements.thumbnails.innerHTML = state.slides
|
||
.map(
|
||
(slide, index) => `
|
||
<div class="slide-thumbnail ${index === state.currentSlideIndex ? "active" : ""}"
|
||
data-index="${index}"
|
||
onclick="window.slidesApp.goToSlide(${index})"
|
||
oncontextmenu="window.slidesApp.showSlideContextMenu(event, ${index})">
|
||
<div class="slide-thumbnail-preview" id="thumbnail-${index}">
|
||
${renderSlideThumbnailContent(slide)}
|
||
</div>
|
||
<span class="slide-thumbnail-number">${index + 1}</span>
|
||
</div>
|
||
`,
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
function renderSlideThumbnailContent(slide) {
|
||
const scale = 0.15;
|
||
let html = `<div style="transform: scale(${scale}); transform-origin: top left; width: ${CONFIG.CANVAS_WIDTH}px; height: ${CONFIG.CANVAS_HEIGHT}px; background: ${slide.background.color || "#ffffff"}; position: relative;">`;
|
||
|
||
slide.elements.forEach((element) => {
|
||
html += renderElementHTML(element, true);
|
||
});
|
||
|
||
html += "</div>";
|
||
return html;
|
||
}
|
||
|
||
function renderCurrentSlide() {
|
||
if (!elements.canvas) return;
|
||
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (!slide) return;
|
||
|
||
elements.canvas.style.background = slide.background.color || "#ffffff";
|
||
elements.canvas.innerHTML = "";
|
||
|
||
slide.elements.forEach((element) => {
|
||
const el = document.createElement("div");
|
||
el.innerHTML = renderElementHTML(element);
|
||
const elementNode = el.firstElementChild;
|
||
if (elementNode) {
|
||
elements.canvas.appendChild(elementNode);
|
||
bindElementEvents(elementNode, element);
|
||
}
|
||
});
|
||
|
||
clearSelection();
|
||
updateSlideCounter();
|
||
}
|
||
|
||
function renderElementHTML(element, isThumbnail = false) {
|
||
const style = buildElementStyle(element);
|
||
const classes = ["slide-element"];
|
||
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.id === element.id &&
|
||
!isThumbnail
|
||
) {
|
||
classes.push("selected");
|
||
}
|
||
if (element.locked) {
|
||
classes.push("locked");
|
||
}
|
||
|
||
let content = "";
|
||
|
||
switch (element.element_type) {
|
||
case "text":
|
||
classes.push("slide-element-text");
|
||
content = escapeHtml(element.content.text || "").replace(/\n/g, "<br>");
|
||
break;
|
||
case "image":
|
||
classes.push("slide-element-image");
|
||
content = `<img src="${element.content.src}" alt="" draggable="false">`;
|
||
break;
|
||
case "shape":
|
||
classes.push("slide-element-shape");
|
||
content = renderShapeSVG(element);
|
||
break;
|
||
case "chart":
|
||
classes.push("slide-element-chart");
|
||
content = renderChartContent(element);
|
||
break;
|
||
}
|
||
|
||
return `
|
||
<div class="${classes.join(" ")}"
|
||
data-id="${element.id}"
|
||
style="${style}">
|
||
${content}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function buildElementStyle(element) {
|
||
const styles = [
|
||
`left: ${element.x}px`,
|
||
`top: ${element.y}px`,
|
||
`width: ${element.width}px`,
|
||
`height: ${element.height}px`,
|
||
`transform: rotate(${element.rotation || 0}deg)`,
|
||
`z-index: ${element.z_index || 1}`,
|
||
];
|
||
|
||
const s = element.style || {};
|
||
|
||
if (element.element_type === "text") {
|
||
if (s.fontFamily) styles.push(`font-family: ${s.fontFamily}`);
|
||
if (s.fontSize) styles.push(`font-size: ${s.fontSize}px`);
|
||
if (s.fontWeight) styles.push(`font-weight: ${s.fontWeight}`);
|
||
if (s.fontStyle) styles.push(`font-style: ${s.fontStyle}`);
|
||
if (s.textAlign) styles.push(`text-align: ${s.textAlign}`);
|
||
if (s.color) styles.push(`color: ${s.color}`);
|
||
if (s.lineHeight) styles.push(`line-height: ${s.lineHeight}`);
|
||
if (s.fill) styles.push(`background: ${s.fill}`);
|
||
}
|
||
|
||
if (element.element_type === "shape") {
|
||
if (s.opacity) styles.push(`opacity: ${s.opacity}`);
|
||
}
|
||
|
||
return styles.join("; ");
|
||
}
|
||
|
||
function renderShapeSVG(element) {
|
||
const shapeType = element.content.shape_type || "rectangle";
|
||
const fill = element.style.fill || "#3b82f6";
|
||
const stroke = element.style.stroke || "none";
|
||
const strokeWidth = element.style.strokeWidth || 0;
|
||
|
||
let path = "";
|
||
switch (shapeType) {
|
||
case "rectangle":
|
||
path = `<rect x="0" y="0" width="100%" height="100%" rx="${element.style.borderRadius || 0}"/>`;
|
||
break;
|
||
case "rounded-rectangle":
|
||
path = `<rect x="0" y="0" width="100%" height="100%" rx="12"/>`;
|
||
break;
|
||
case "ellipse":
|
||
path = `<ellipse cx="50%" cy="50%" rx="50%" ry="50%"/>`;
|
||
break;
|
||
case "triangle":
|
||
path = `<polygon points="50,0 100,100 0,100"/>`;
|
||
break;
|
||
case "diamond":
|
||
path = `<polygon points="50,0 100,50 50,100 0,50"/>`;
|
||
break;
|
||
case "star":
|
||
path = `<polygon points="50,0 61,35 98,35 68,57 79,91 50,70 21,91 32,57 2,35 39,35"/>`;
|
||
break;
|
||
case "arrow-right":
|
||
path = `<polygon points="0,25 60,25 60,0 100,50 60,100 60,75 0,75"/>`;
|
||
break;
|
||
case "callout":
|
||
path = `<path d="M0,0 L100,0 L100,70 L40,70 L20,100 L20,70 L0,70 Z"/>`;
|
||
break;
|
||
default:
|
||
path = `<rect x="0" y="0" width="100%" height="100%"/>`;
|
||
}
|
||
|
||
return `
|
||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="fill: ${fill}; stroke: ${stroke}; stroke-width: ${strokeWidth};">
|
||
${path}
|
||
</svg>
|
||
`;
|
||
}
|
||
|
||
function renderChartContent(element) {
|
||
return '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#999;">Chart</div>';
|
||
}
|
||
|
||
function bindElementEvents(node, element) {
|
||
node.addEventListener("mousedown", (e) =>
|
||
handleElementMouseDown(e, element),
|
||
);
|
||
node.addEventListener("dblclick", (e) =>
|
||
handleElementDoubleClick(e, element),
|
||
);
|
||
}
|
||
|
||
function handleCanvasMouseDown(e) {
|
||
if (e.target === elements.canvas) {
|
||
clearSelection();
|
||
}
|
||
}
|
||
|
||
function handleCanvasDoubleClick(e) {
|
||
if (e.target === elements.canvas) {
|
||
const rect = elements.canvas.getBoundingClientRect();
|
||
const x = (e.clientX - rect.left) / state.zoom;
|
||
const y = (e.clientY - rect.top) / state.zoom;
|
||
addTextBoxAt(x - 100, y - 25);
|
||
}
|
||
}
|
||
|
||
function addTextBox() {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const centerX = CONFIG.CANVAS_WIDTH / 2 - 150;
|
||
const centerY = CONFIG.CANVAS_HEIGHT / 2 - 30;
|
||
addTextBoxAt(centerX, centerY);
|
||
}
|
||
|
||
function addTextBoxAt(x, y) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const textElement = createTextElement(x, y, 300, 60, "Click to edit text", {
|
||
fontSize: 24,
|
||
color: "#1e293b",
|
||
});
|
||
slide.elements.push(textElement);
|
||
saveToHistory();
|
||
renderCurrentSlide();
|
||
selectElement(textElement);
|
||
scheduleAutoSave();
|
||
broadcastChange("elementAdded", { element: textElement });
|
||
}
|
||
|
||
function handleElementMouseDown(e, element) {
|
||
e.stopPropagation();
|
||
|
||
if (element.locked) return;
|
||
|
||
selectElement(element);
|
||
|
||
if (e.button === 0) {
|
||
state.isDragging = true;
|
||
state.dragStart = {
|
||
x: e.clientX,
|
||
y: e.clientY,
|
||
elementX: element.x,
|
||
elementY: element.y,
|
||
};
|
||
}
|
||
}
|
||
|
||
function handleElementDoubleClick(e, element) {
|
||
e.stopPropagation();
|
||
|
||
if (element.element_type === "text") {
|
||
startTextEditing(element);
|
||
}
|
||
}
|
||
|
||
function handleResizeStart(e) {
|
||
e.stopPropagation();
|
||
|
||
if (!state.selectedElement) return;
|
||
|
||
const handle = e.target.dataset.handle;
|
||
if (handle === "rotate") {
|
||
state.isRotating = true;
|
||
} else {
|
||
state.isResizing = true;
|
||
state.resizeHandle = handle;
|
||
}
|
||
|
||
state.dragStart = {
|
||
x: e.clientX,
|
||
y: e.clientY,
|
||
elementX: state.selectedElement.x,
|
||
elementY: state.selectedElement.y,
|
||
elementWidth: state.selectedElement.width,
|
||
elementHeight: state.selectedElement.height,
|
||
elementRotation: state.selectedElement.rotation || 0,
|
||
};
|
||
}
|
||
|
||
function handleMouseMove(e) {
|
||
if (state.isDragging && state.selectedElement && state.dragStart) {
|
||
const dx = (e.clientX - state.dragStart.x) / state.zoom;
|
||
const dy = (e.clientY - state.dragStart.y) / state.zoom;
|
||
|
||
state.selectedElement.x = state.dragStart.elementX + dx;
|
||
state.selectedElement.y = state.dragStart.elementY + dy;
|
||
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
broadcastChange("elementMove", state.selectedElement);
|
||
} else if (state.isResizing && state.selectedElement && state.dragStart) {
|
||
const dx = (e.clientX - state.dragStart.x) / state.zoom;
|
||
const dy = (e.clientY - state.dragStart.y) / state.zoom;
|
||
|
||
resizeElement(dx, dy);
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
broadcastChange("elementResize", state.selectedElement);
|
||
} else if (state.isRotating && state.selectedElement) {
|
||
const rect = elements.canvas.getBoundingClientRect();
|
||
const centerX = state.selectedElement.x + state.selectedElement.width / 2;
|
||
const centerY =
|
||
state.selectedElement.y + state.selectedElement.height / 2;
|
||
const mouseX = (e.clientX - rect.left) / state.zoom;
|
||
const mouseY = (e.clientY - rect.top) / state.zoom;
|
||
|
||
const angle =
|
||
Math.atan2(mouseY - centerY, mouseX - centerX) * (180 / Math.PI) + 90;
|
||
state.selectedElement.rotation = Math.round(angle);
|
||
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
updatePropertiesPanel();
|
||
broadcastChange("elementRotate", state.selectedElement);
|
||
}
|
||
|
||
broadcastCursor(e);
|
||
}
|
||
|
||
function resizeElement(dx, dy) {
|
||
const el = state.selectedElement;
|
||
const s = state.dragStart;
|
||
|
||
switch (state.resizeHandle) {
|
||
case "se":
|
||
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth + dx);
|
||
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight + dy);
|
||
break;
|
||
case "sw":
|
||
el.x = s.elementX + dx;
|
||
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth - dx);
|
||
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight + dy);
|
||
break;
|
||
case "ne":
|
||
el.y = s.elementY + dy;
|
||
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth + dx);
|
||
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight - dy);
|
||
break;
|
||
case "nw":
|
||
el.x = s.elementX + dx;
|
||
el.y = s.elementY + dy;
|
||
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth - dx);
|
||
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight - dy);
|
||
break;
|
||
case "n":
|
||
el.y = s.elementY + dy;
|
||
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight - dy);
|
||
break;
|
||
case "s":
|
||
el.height = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementHeight + dy);
|
||
break;
|
||
case "e":
|
||
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth + dx);
|
||
break;
|
||
case "w":
|
||
el.x = s.elementX + dx;
|
||
el.width = Math.max(CONFIG.MIN_ELEMENT_SIZE, s.elementWidth - dx);
|
||
break;
|
||
}
|
||
}
|
||
|
||
function handleMouseUp() {
|
||
if (state.isDragging || state.isResizing || state.isRotating) {
|
||
saveToHistory();
|
||
scheduleAutoSave();
|
||
}
|
||
|
||
state.isDragging = false;
|
||
state.isResizing = false;
|
||
state.isRotating = false;
|
||
state.dragStart = null;
|
||
state.resizeHandle = null;
|
||
}
|
||
|
||
function handleKeyDown(e) {
|
||
if (
|
||
e.target.tagName === "INPUT" ||
|
||
e.target.tagName === "TEXTAREA" ||
|
||
e.target.isContentEditable
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const isMod = e.ctrlKey || e.metaKey;
|
||
|
||
if (isMod && e.key === "z") {
|
||
e.preventDefault();
|
||
if (e.shiftKey) {
|
||
redo();
|
||
} else {
|
||
undo();
|
||
}
|
||
} else if (isMod && e.key === "y") {
|
||
e.preventDefault();
|
||
redo();
|
||
} else if (isMod && e.key === "c") {
|
||
e.preventDefault();
|
||
copyElement();
|
||
} else if (isMod && e.key === "x") {
|
||
e.preventDefault();
|
||
cutElement();
|
||
} else if (isMod && e.key === "v") {
|
||
e.preventDefault();
|
||
pasteElement();
|
||
} else if (isMod && e.key === "d") {
|
||
e.preventDefault();
|
||
duplicateElement();
|
||
} else if (isMod && e.key === "s") {
|
||
e.preventDefault();
|
||
savePresentation();
|
||
} else if (isMod && e.key === "a") {
|
||
e.preventDefault();
|
||
selectAll();
|
||
} else if (e.key === "Delete" || e.key === "Backspace") {
|
||
if (state.selectedElement) {
|
||
e.preventDefault();
|
||
deleteElement();
|
||
}
|
||
} else if (e.key === "Escape") {
|
||
clearSelection();
|
||
hideContextMenus();
|
||
if (state.isPresenting) {
|
||
exitPresentation();
|
||
}
|
||
} else if (e.key === "ArrowUp" && state.selectedElement) {
|
||
e.preventDefault();
|
||
state.selectedElement.y -= e.shiftKey ? 10 : 1;
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
} else if (e.key === "ArrowDown" && state.selectedElement) {
|
||
e.preventDefault();
|
||
state.selectedElement.y += e.shiftKey ? 10 : 1;
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
} else if (e.key === "ArrowLeft" && state.selectedElement) {
|
||
e.preventDefault();
|
||
state.selectedElement.x -= e.shiftKey ? 10 : 1;
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
} else if (e.key === "ArrowRight" && state.selectedElement) {
|
||
e.preventDefault();
|
||
state.selectedElement.x += e.shiftKey ? 10 : 1;
|
||
updateElementPosition(state.selectedElement);
|
||
updateSelectionHandles();
|
||
} else if (e.key === "F5") {
|
||
e.preventDefault();
|
||
startPresentation();
|
||
} else if (
|
||
e.key === "PageDown" ||
|
||
(e.key === "ArrowRight" && !state.selectedElement)
|
||
) {
|
||
e.preventDefault();
|
||
goToSlide(state.currentSlideIndex + 1);
|
||
} else if (
|
||
e.key === "PageUp" ||
|
||
(e.key === "ArrowLeft" && !state.selectedElement)
|
||
) {
|
||
e.preventDefault();
|
||
goToSlide(state.currentSlideIndex - 1);
|
||
}
|
||
}
|
||
|
||
function selectElement(element) {
|
||
state.selectedElement = element;
|
||
|
||
document.querySelectorAll(".slide-element.selected").forEach((el) => {
|
||
el.classList.remove("selected");
|
||
});
|
||
|
||
const node = document.querySelector(`[data-id="${element.id}"]`);
|
||
if (node) {
|
||
node.classList.add("selected");
|
||
}
|
||
|
||
updateSelectionHandles();
|
||
updatePropertiesPanel();
|
||
showPropertiesPanel();
|
||
}
|
||
|
||
function clearSelection() {
|
||
state.selectedElement = null;
|
||
|
||
document.querySelectorAll(".slide-element.selected").forEach((el) => {
|
||
el.classList.remove("selected");
|
||
});
|
||
|
||
hideSelectionHandles();
|
||
updatePropertiesPanel();
|
||
}
|
||
|
||
function updateSelectionHandles() {
|
||
if (!state.selectedElement || !elements.selectionHandles) {
|
||
hideSelectionHandles();
|
||
return;
|
||
}
|
||
|
||
const el = state.selectedElement;
|
||
elements.selectionHandles.classList.remove("hidden");
|
||
elements.selectionHandles.style.left = `${el.x}px`;
|
||
elements.selectionHandles.style.top = `${el.y}px`;
|
||
elements.selectionHandles.style.width = `${el.width}px`;
|
||
elements.selectionHandles.style.height = `${el.height}px`;
|
||
elements.selectionHandles.style.transform = `rotate(${el.rotation || 0}deg)`;
|
||
}
|
||
|
||
function hideSelectionHandles() {
|
||
if (elements.selectionHandles) {
|
||
elements.selectionHandles.classList.add("hidden");
|
||
}
|
||
}
|
||
|
||
function updateElementPosition(element) {
|
||
const node = document.querySelector(`[data-id="${element.id}"]`);
|
||
if (node) {
|
||
node.style.left = `${element.x}px`;
|
||
node.style.top = `${element.y}px`;
|
||
node.style.width = `${element.width}px`;
|
||
node.style.height = `${element.height}px`;
|
||
node.style.transform = `rotate(${element.rotation || 0}deg)`;
|
||
}
|
||
state.isDirty = true;
|
||
}
|
||
|
||
function updatePropertiesPanel() {
|
||
if (!state.selectedElement) {
|
||
document.getElementById("prop-x").value = "";
|
||
document.getElementById("prop-y").value = "";
|
||
document.getElementById("prop-width").value = "";
|
||
document.getElementById("prop-height").value = "";
|
||
document.getElementById("prop-rotation").value = 0;
|
||
document.getElementById("rotation-value").textContent = "0°";
|
||
document.getElementById("prop-opacity").value = 100;
|
||
document.getElementById("opacity-value").textContent = "100%";
|
||
return;
|
||
}
|
||
|
||
const el = state.selectedElement;
|
||
document.getElementById("prop-x").value = Math.round(el.x);
|
||
document.getElementById("prop-y").value = Math.round(el.y);
|
||
document.getElementById("prop-width").value = Math.round(el.width);
|
||
document.getElementById("prop-height").value = Math.round(el.height);
|
||
document.getElementById("prop-rotation").value = el.rotation || 0;
|
||
document.getElementById("rotation-value").textContent =
|
||
`${el.rotation || 0}°`;
|
||
|
||
const opacity = (el.style.opacity || 1) * 100;
|
||
document.getElementById("prop-opacity").value = opacity;
|
||
document.getElementById("opacity-value").textContent =
|
||
`${Math.round(opacity)}%`;
|
||
}
|
||
|
||
function showPropertiesPanel() {
|
||
if (elements.propertiesPanel) {
|
||
elements.propertiesPanel.classList.remove("collapsed");
|
||
}
|
||
}
|
||
|
||
function startTextEditing(element) {
|
||
const node = document.querySelector(`[data-id="${element.id}"]`);
|
||
if (!node) return;
|
||
|
||
node.contentEditable = true;
|
||
node.classList.add("editing");
|
||
node.focus();
|
||
|
||
const range = document.createRange();
|
||
range.selectNodeContents(node);
|
||
const sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
|
||
node.addEventListener(
|
||
"blur",
|
||
() => {
|
||
node.contentEditable = false;
|
||
node.classList.remove("editing");
|
||
element.content.text = node.innerText;
|
||
saveToHistory();
|
||
scheduleAutoSave();
|
||
renderThumbnails();
|
||
},
|
||
{ once: true },
|
||
);
|
||
}
|
||
|
||
function goToSlide(index) {
|
||
if (index < 0 || index >= state.slides.length) return;
|
||
|
||
state.currentSlideIndex = index;
|
||
renderCurrentSlide();
|
||
renderThumbnails();
|
||
updateSlideCounter();
|
||
broadcastChange("slideChange", { slideIndex: index });
|
||
}
|
||
|
||
function addSlide(layout = "title-content") {
|
||
const newSlide = createSlide(layout);
|
||
state.slides.splice(state.currentSlideIndex + 1, 0, newSlide);
|
||
state.currentSlideIndex++;
|
||
saveToHistory();
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
scheduleAutoSave();
|
||
broadcastChange("slideAdded", { slideIndex: state.currentSlideIndex });
|
||
}
|
||
|
||
function duplicateSlide() {
|
||
const currentSlide = state.slides[state.currentSlideIndex];
|
||
const duplicated = JSON.parse(JSON.stringify(currentSlide));
|
||
duplicated.id = generateId();
|
||
duplicated.elements.forEach((el) => {
|
||
el.id = generateId();
|
||
});
|
||
state.slides.splice(state.currentSlideIndex + 1, 0, duplicated);
|
||
state.currentSlideIndex++;
|
||
saveToHistory();
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
scheduleAutoSave();
|
||
}
|
||
|
||
function deleteSlide() {
|
||
if (state.slides.length <= 1) return;
|
||
|
||
state.slides.splice(state.currentSlideIndex, 1);
|
||
if (state.currentSlideIndex >= state.slides.length) {
|
||
state.currentSlideIndex = state.slides.length - 1;
|
||
}
|
||
saveToHistory();
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
scheduleAutoSave();
|
||
broadcastChange("slideDeleted", { slideIndex: state.currentSlideIndex });
|
||
}
|
||
|
||
function updateSlideCounter() {
|
||
const currentEl = document.getElementById("current-slide-num");
|
||
const totalEl = document.getElementById("total-slides-num");
|
||
if (currentEl) currentEl.textContent = state.currentSlideIndex + 1;
|
||
if (totalEl) totalEl.textContent = state.slides.length;
|
||
}
|
||
|
||
function showImageModal() {
|
||
const url = prompt("Enter image URL:");
|
||
if (url) {
|
||
addImage(url);
|
||
}
|
||
}
|
||
|
||
function addImage(url) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const imageElement = createImageElement(100, 100, 400, 300, url);
|
||
slide.elements.push(imageElement);
|
||
saveToHistory();
|
||
renderCurrentSlide();
|
||
selectElement(imageElement);
|
||
scheduleAutoSave();
|
||
}
|
||
|
||
function showShapeModal() {
|
||
addShape("rectangle");
|
||
}
|
||
|
||
function addShape(shapeType) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const shapeElement = createShapeElement(100, 100, 200, 150, shapeType, {
|
||
fill: "#3b82f6",
|
||
});
|
||
slide.elements.push(shapeElement);
|
||
saveToHistory();
|
||
renderCurrentSlide();
|
||
selectElement(shapeElement);
|
||
scheduleAutoSave();
|
||
}
|
||
|
||
function showChartModal() {
|
||
alert("Chart insertion coming soon!");
|
||
}
|
||
|
||
function addTable() {
|
||
alert("Table insertion coming soon!");
|
||
}
|
||
|
||
function setFontFamily(family) {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.fontFamily = family;
|
||
renderCurrentSlide();
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function setFontSize(size) {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.fontSize = parseInt(size, 10);
|
||
renderCurrentSlide();
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function toggleBold() {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.fontWeight =
|
||
state.selectedElement.style.fontWeight === "bold" ? "normal" : "bold";
|
||
renderCurrentSlide();
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function toggleItalic() {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.fontStyle =
|
||
state.selectedElement.style.fontStyle === "italic"
|
||
? "normal"
|
||
: "italic";
|
||
renderCurrentSlide();
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function toggleUnderline() {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.textDecoration =
|
||
state.selectedElement.style.textDecoration === "underline"
|
||
? "none"
|
||
: "underline";
|
||
renderCurrentSlide();
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function startPresentation() {
|
||
state.isPresenting = true;
|
||
if (elements.presenterModal) {
|
||
elements.presenterModal.classList.remove("hidden");
|
||
renderPresenterSlide();
|
||
}
|
||
document.addEventListener("keydown", handlePresenterKeyDown);
|
||
}
|
||
|
||
function exitPresentation() {
|
||
state.isPresenting = false;
|
||
if (elements.presenterModal) {
|
||
elements.presenterModal.classList.add("hidden");
|
||
}
|
||
document.removeEventListener("keydown", handlePresenterKeyDown);
|
||
}
|
||
|
||
function handlePresenterKeyDown(e) {
|
||
if (e.key === "Escape") {
|
||
exitPresentation();
|
||
} else if (e.key === "ArrowRight" || e.key === " ") {
|
||
navigatePresentation(1);
|
||
} else if (e.key === "ArrowLeft") {
|
||
navigatePresentation(-1);
|
||
}
|
||
}
|
||
|
||
function navigatePresentation(direction) {
|
||
const newIndex = state.currentSlideIndex + direction;
|
||
if (newIndex >= 0 && newIndex < state.slides.length) {
|
||
goToSlide(newIndex);
|
||
if (state.isPresenting) {
|
||
renderPresenterSlide();
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderPresenterSlide() {
|
||
const presenterSlide = document.getElementById("presenterSlide");
|
||
const presenterSlideNumber = document.getElementById(
|
||
"presenterSlideNumber",
|
||
);
|
||
if (presenterSlide && state.slides[state.currentSlideIndex]) {
|
||
presenterSlide.innerHTML = renderSlideContent(
|
||
state.slides[state.currentSlideIndex],
|
||
);
|
||
}
|
||
if (presenterSlideNumber) {
|
||
presenterSlideNumber.textContent = `${state.currentSlideIndex + 1} / ${state.slides.length}`;
|
||
}
|
||
}
|
||
|
||
function renderSlideContent(slide) {
|
||
let html = "";
|
||
if (slide.elements) {
|
||
slide.elements.forEach((el) => {
|
||
html += renderElementHTML(el);
|
||
});
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function zoomIn() {
|
||
if (state.zoom < 200) {
|
||
state.zoom += 10;
|
||
applyZoom();
|
||
}
|
||
}
|
||
|
||
function zoomOut() {
|
||
if (state.zoom > 50) {
|
||
state.zoom -= 10;
|
||
applyZoom();
|
||
}
|
||
}
|
||
|
||
function applyZoom() {
|
||
if (elements.slideCanvas) {
|
||
elements.slideCanvas.style.transform = `scale(${state.zoom / 100})`;
|
||
}
|
||
if (elements.zoomLevel) {
|
||
elements.zoomLevel.textContent = `${state.zoom}%`;
|
||
}
|
||
}
|
||
|
||
function toggleChatPanel() {
|
||
state.chatPanelOpen = !state.chatPanelOpen;
|
||
elements.chatPanel?.classList.toggle("collapsed", !state.chatPanelOpen);
|
||
}
|
||
|
||
function handleChatSubmit(e) {
|
||
e.preventDefault();
|
||
const message = elements.chatInput?.value.trim();
|
||
if (!message) return;
|
||
|
||
addChatMessage("user", message);
|
||
if (elements.chatInput) elements.chatInput.value = "";
|
||
|
||
processAICommand(message);
|
||
}
|
||
|
||
function handleSuggestionClick(action) {
|
||
const commands = {
|
||
title: "Add a title slide",
|
||
image: "Insert an image",
|
||
duplicate: "Duplicate this slide",
|
||
notes: "Add speaker notes",
|
||
};
|
||
|
||
const message = commands[action] || action;
|
||
addChatMessage("user", message);
|
||
processAICommand(message);
|
||
}
|
||
|
||
function addChatMessage(role, content) {
|
||
if (!elements.chatMessages) return;
|
||
const div = document.createElement("div");
|
||
div.className = `chat-message ${role}`;
|
||
div.innerHTML = `<div class="message-bubble">${escapeHtml(content)}</div>`;
|
||
elements.chatMessages.appendChild(div);
|
||
elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
|
||
}
|
||
|
||
async function processAICommand(command) {
|
||
const lower = command.toLowerCase();
|
||
let response = "";
|
||
|
||
if (lower.includes("title") && lower.includes("slide")) {
|
||
addSlide("title");
|
||
response = "Added a new title slide!";
|
||
} else if (lower.includes("add") && lower.includes("slide")) {
|
||
addSlide();
|
||
response = "Added a new blank slide!";
|
||
} else if (lower.includes("duplicate")) {
|
||
duplicateSlide();
|
||
response = "Duplicated the current slide!";
|
||
} else if (lower.includes("delete") && lower.includes("slide")) {
|
||
if (state.slides.length > 1) {
|
||
deleteSlide();
|
||
response = "Deleted the current slide!";
|
||
} else {
|
||
response = "Cannot delete the only slide in the presentation.";
|
||
}
|
||
} else if (lower.includes("image") || lower.includes("picture")) {
|
||
showModal("imageModal");
|
||
response = "Opening image dialog. Enter the image URL to insert.";
|
||
} else if (lower.includes("shape")) {
|
||
showModal("shapeModal");
|
||
response = "Opening shape picker. Choose a shape to insert.";
|
||
} else if (lower.includes("text") || lower.includes("text box")) {
|
||
addTextBox();
|
||
response = "Added a text box! Double-click to edit the text.";
|
||
} else if (lower.includes("background")) {
|
||
showModal("backgroundModal");
|
||
response = "Opening background settings. Choose a color or image.";
|
||
} else if (lower.includes("notes") || lower.includes("speaker")) {
|
||
showModal("notesModal");
|
||
const currentSlide = state.slides[state.currentSlideIndex];
|
||
const notesInput = document.getElementById("speakerNotes");
|
||
if (notesInput && currentSlide) {
|
||
notesInput.value = currentSlide.notes || "";
|
||
}
|
||
response = "Opening speaker notes. Add notes for this slide.";
|
||
} else if (lower.includes("present") || lower.includes("start")) {
|
||
startPresentation();
|
||
response = "Starting presentation mode! Press Esc to exit.";
|
||
} else if (lower.includes("bigger") || lower.includes("larger")) {
|
||
if (state.selectedElement) {
|
||
state.selectedElement.width =
|
||
(state.selectedElement.width || 200) * 1.2;
|
||
state.selectedElement.height =
|
||
(state.selectedElement.height || 100) * 1.2;
|
||
renderCurrentSlide();
|
||
response = "Made the selected element larger!";
|
||
} else {
|
||
response = "Please select an element first.";
|
||
}
|
||
} else if (lower.includes("smaller")) {
|
||
if (state.selectedElement) {
|
||
state.selectedElement.width =
|
||
(state.selectedElement.width || 200) * 0.8;
|
||
state.selectedElement.height =
|
||
(state.selectedElement.height || 100) * 0.8;
|
||
renderCurrentSlide();
|
||
response = "Made the selected element smaller!";
|
||
} else {
|
||
response = "Please select an element first.";
|
||
}
|
||
} else if (lower.includes("center")) {
|
||
if (state.selectedElement) {
|
||
state.selectedElement.x =
|
||
(CONFIG.CANVAS_WIDTH - (state.selectedElement.width || 200)) / 2;
|
||
state.selectedElement.y =
|
||
(CONFIG.CANVAS_HEIGHT - (state.selectedElement.height || 100)) / 2;
|
||
renderCurrentSlide();
|
||
response = "Centered the selected element!";
|
||
} else {
|
||
response = "Please select an element first.";
|
||
}
|
||
} else if (lower.includes("bold")) {
|
||
toggleBold();
|
||
response = "Toggled bold formatting!";
|
||
} else if (lower.includes("italic")) {
|
||
toggleItalic();
|
||
response = "Toggled italic formatting!";
|
||
} else {
|
||
try {
|
||
const res = await fetch("/api/slides/ai", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
command,
|
||
slideIndex: state.currentSlideIndex,
|
||
presentationId: state.presentationId,
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
response = data.response || "I processed your request.";
|
||
} catch {
|
||
response =
|
||
"I can help you with:\n• Add/duplicate/delete slides\n• Insert text, images, shapes\n• Change slide background\n• Add speaker notes\n• Make elements bigger/smaller\n• Center elements\n• Start presentation";
|
||
}
|
||
}
|
||
|
||
addChatMessage("assistant", response);
|
||
}
|
||
|
||
function showModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
if (modal) modal.classList.remove("hidden");
|
||
}
|
||
|
||
function hideModal(modalId) {
|
||
const modal = document.getElementById(modalId);
|
||
if (modal) modal.classList.add("hidden");
|
||
}
|
||
|
||
function insertImage() {
|
||
const url = document.getElementById("imageUrl")?.value;
|
||
const alt = document.getElementById("imageAlt")?.value || "Image";
|
||
if (url) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
const imageElement = createImageElement(url, 100, 100, 400, 300);
|
||
slide.elements.push(imageElement);
|
||
renderCurrentSlide();
|
||
renderThumbnails();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
hideModal("imageModal");
|
||
}
|
||
}
|
||
|
||
function saveNotes() {
|
||
const notes = document.getElementById("speakerNotes")?.value || "";
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
slide.notes = notes;
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
hideModal("notesModal");
|
||
addChatMessage("assistant", "Speaker notes saved!");
|
||
}
|
||
|
||
function applyBackground() {
|
||
const color = document.getElementById("bgColor")?.value;
|
||
const imageUrl = document.getElementById("bgImageUrl")?.value;
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
|
||
if (slide) {
|
||
if (imageUrl) {
|
||
slide.background = { bg_type: "image", url: imageUrl };
|
||
} else if (color) {
|
||
slide.background = { bg_type: "solid", color };
|
||
}
|
||
renderCurrentSlide();
|
||
renderThumbnails();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
hideModal("backgroundModal");
|
||
addChatMessage("assistant", "Slide background updated!");
|
||
}
|
||
|
||
function copyShareLink() {
|
||
const linkInput = document.getElementById("shareLink");
|
||
if (linkInput) {
|
||
const shareUrl = `${window.location.origin}${window.location.pathname}#id=${state.presentationId || "new"}`;
|
||
linkInput.value = shareUrl;
|
||
linkInput.select();
|
||
navigator.clipboard.writeText(shareUrl);
|
||
addChatMessage("assistant", "Share link copied to clipboard!");
|
||
}
|
||
}
|
||
|
||
function handleContextMenu(e) {
|
||
e.preventDefault();
|
||
const target = e.target.closest(".slide-element");
|
||
const thumbnail = e.target.closest(".slide-thumbnail");
|
||
|
||
hideAllContextMenus();
|
||
|
||
if (target) {
|
||
const elementId = target.dataset.id;
|
||
selectElement(elementId);
|
||
showContextMenu(elements.contextMenu, e.clientX, e.clientY);
|
||
} else if (thumbnail) {
|
||
showContextMenu(elements.slideContextMenu, e.clientX, e.clientY);
|
||
}
|
||
}
|
||
|
||
function hideAllContextMenus() {
|
||
elements.contextMenu?.classList.add("hidden");
|
||
elements.slideContextMenu?.classList.add("hidden");
|
||
}
|
||
|
||
function showContextMenu(menu, x, y) {
|
||
if (!menu) return;
|
||
menu.style.left = `${x}px`;
|
||
menu.style.top = `${y}px`;
|
||
menu.classList.remove("hidden");
|
||
}
|
||
|
||
function handleDocumentClick(e) {
|
||
if (!e.target.closest(".context-menu")) {
|
||
hideAllContextMenus();
|
||
}
|
||
}
|
||
|
||
function handleContextAction(action) {
|
||
hideAllContextMenus();
|
||
|
||
switch (action) {
|
||
case "cut":
|
||
cutElement();
|
||
break;
|
||
case "copy":
|
||
copyElement();
|
||
break;
|
||
case "paste":
|
||
pasteElement();
|
||
break;
|
||
case "duplicate":
|
||
duplicateElement();
|
||
break;
|
||
case "delete":
|
||
deleteElement();
|
||
break;
|
||
case "bringFront":
|
||
bringToFront();
|
||
break;
|
||
case "sendBack":
|
||
sendToBack();
|
||
break;
|
||
case "newSlide":
|
||
addSlide();
|
||
break;
|
||
case "duplicateSlide":
|
||
duplicateSlide();
|
||
break;
|
||
case "deleteSlide":
|
||
deleteSlide();
|
||
break;
|
||
case "slideBackground":
|
||
showModal("backgroundModal");
|
||
break;
|
||
case "slideNotes":
|
||
showModal("notesModal");
|
||
break;
|
||
}
|
||
}
|
||
|
||
function cutElement() {
|
||
if (state.selectedElement) {
|
||
state.clipboard = JSON.parse(JSON.stringify(state.selectedElement));
|
||
deleteElement();
|
||
}
|
||
}
|
||
|
||
function copyElement() {
|
||
if (state.selectedElement) {
|
||
state.clipboard = JSON.parse(JSON.stringify(state.selectedElement));
|
||
addChatMessage("assistant", "Element copied!");
|
||
}
|
||
}
|
||
|
||
function pasteElement() {
|
||
if (state.clipboard) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
const newElement = JSON.parse(JSON.stringify(state.clipboard));
|
||
newElement.id = generateId();
|
||
newElement.x += 20;
|
||
newElement.y += 20;
|
||
slide.elements.push(newElement);
|
||
renderCurrentSlide();
|
||
renderThumbnails();
|
||
selectElement(newElement.id);
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
}
|
||
|
||
function duplicateElement() {
|
||
if (state.selectedElement) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
const newElement = JSON.parse(JSON.stringify(state.selectedElement));
|
||
newElement.id = generateId();
|
||
newElement.x += 20;
|
||
newElement.y += 20;
|
||
slide.elements.push(newElement);
|
||
renderCurrentSlide();
|
||
renderThumbnails();
|
||
selectElement(newElement.id);
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
}
|
||
|
||
function deleteElement() {
|
||
if (state.selectedElement) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
slide.elements = slide.elements.filter(
|
||
(el) => el.id !== state.selectedElement.id,
|
||
);
|
||
clearSelection();
|
||
renderCurrentSlide();
|
||
renderThumbnails();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
}
|
||
|
||
function bringToFront() {
|
||
if (state.selectedElement) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
const index = slide.elements.findIndex(
|
||
(el) => el.id === state.selectedElement.id,
|
||
);
|
||
if (index > -1) {
|
||
const [element] = slide.elements.splice(index, 1);
|
||
slide.elements.push(element);
|
||
renderCurrentSlide();
|
||
state.isDirty = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function sendToBack() {
|
||
if (state.selectedElement) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
if (slide) {
|
||
const index = slide.elements.findIndex(
|
||
(el) => el.id === state.selectedElement.id,
|
||
);
|
||
if (index > -1) {
|
||
const [element] = slide.elements.splice(index, 1);
|
||
slide.elements.unshift(element);
|
||
renderCurrentSlide();
|
||
state.isDirty = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function setTextColor(color) {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.color = color;
|
||
renderCurrentSlide();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
const indicator = document.querySelector("#textColorBtn .color-indicator");
|
||
if (indicator) indicator.style.background = color;
|
||
}
|
||
|
||
function setFillColor(color) {
|
||
if (state.selectedElement) {
|
||
if (state.selectedElement.element_type === "shape") {
|
||
state.selectedElement.style.fill = color;
|
||
} else if (state.selectedElement.element_type === "text") {
|
||
state.selectedElement.style.background = color;
|
||
}
|
||
renderCurrentSlide();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
const indicator = document.querySelector("#fillColorBtn .fill-indicator");
|
||
if (indicator) indicator.style.background = color;
|
||
}
|
||
|
||
function setTextAlign(align) {
|
||
if (
|
||
state.selectedElement &&
|
||
state.selectedElement.element_type === "text"
|
||
) {
|
||
state.selectedElement.style.textAlign = align;
|
||
renderCurrentSlide();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function undo() {
|
||
if (state.historyIndex > 0) {
|
||
state.historyIndex--;
|
||
restoreFromHistory();
|
||
}
|
||
}
|
||
|
||
function redo() {
|
||
if (state.historyIndex < state.history.length - 1) {
|
||
state.historyIndex++;
|
||
restoreFromHistory();
|
||
}
|
||
}
|
||
|
||
function saveToHistory() {
|
||
const snapshot = JSON.stringify(state.slides);
|
||
if (state.history[state.historyIndex] === snapshot) return;
|
||
|
||
state.history = state.history.slice(0, state.historyIndex + 1);
|
||
state.history.push(snapshot);
|
||
if (state.history.length > CONFIG.MAX_HISTORY) {
|
||
state.history.shift();
|
||
} else {
|
||
state.historyIndex++;
|
||
}
|
||
}
|
||
|
||
function restoreFromHistory() {
|
||
if (state.history[state.historyIndex]) {
|
||
state.slides = JSON.parse(state.history[state.historyIndex]);
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
}
|
||
}
|
||
|
||
function generateId() {
|
||
return "el-" + Math.random().toString(36).substr(2, 9);
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return "";
|
||
const div = document.createElement("div");
|
||
div.textContent = str;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function scheduleAutoSave() {
|
||
if (state.autoSaveTimer) {
|
||
clearTimeout(state.autoSaveTimer);
|
||
}
|
||
state.autoSaveTimer = setTimeout(savePresentation, CONFIG.AUTOSAVE_DELAY);
|
||
if (elements.saveStatus) {
|
||
elements.saveStatus.textContent = "Saving...";
|
||
}
|
||
}
|
||
|
||
async function savePresentation() {
|
||
if (!state.isDirty) return;
|
||
|
||
try {
|
||
const response = await fetch("/api/slides/save", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
id: state.presentationId,
|
||
name: state.presentationName,
|
||
slides: state.slides,
|
||
theme: state.theme,
|
||
driveSource: state.driveSource,
|
||
}),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
if (result.id) {
|
||
state.presentationId = result.id;
|
||
window.history.replaceState({}, "", `#id=${state.presentationId}`);
|
||
}
|
||
state.isDirty = false;
|
||
if (elements.saveStatus) {
|
||
elements.saveStatus.textContent = "Saved";
|
||
}
|
||
} else {
|
||
if (elements.saveStatus) {
|
||
elements.saveStatus.textContent = "Save failed";
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error("Save error:", e);
|
||
if (elements.saveStatus) {
|
||
elements.saveStatus.textContent = "Save failed";
|
||
}
|
||
}
|
||
}
|
||
|
||
function connectWebSocket() {
|
||
if (!state.presentationId) return;
|
||
|
||
try {
|
||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||
const wsUrl = `${protocol}//${window.location.host}/api/slides/ws/${state.presentationId}`;
|
||
state.ws = new WebSocket(wsUrl);
|
||
|
||
state.ws.onopen = () => {
|
||
state.ws.send(
|
||
JSON.stringify({
|
||
type: "join",
|
||
userId: getUserId(),
|
||
userName: getUserName(),
|
||
}),
|
||
);
|
||
};
|
||
|
||
state.ws.onmessage = (e) => {
|
||
try {
|
||
const msg = JSON.parse(e.data);
|
||
handleWebSocketMessage(msg);
|
||
} catch (err) {
|
||
console.error("WS message error:", err);
|
||
}
|
||
};
|
||
|
||
state.ws.onclose = () => {
|
||
setTimeout(connectWebSocket, CONFIG.WS_RECONNECT_DELAY);
|
||
};
|
||
} catch (e) {
|
||
console.error("WebSocket failed:", e);
|
||
}
|
||
}
|
||
|
||
function handleWebSocketMessage(msg) {
|
||
switch (msg.type) {
|
||
case "user_joined":
|
||
addCollaborator(msg.user);
|
||
break;
|
||
case "user_left":
|
||
removeCollaborator(msg.userId);
|
||
break;
|
||
case "slide_update":
|
||
if (msg.userId !== getUserId()) {
|
||
state.slides = msg.slides;
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
function addCollaborator(user) {
|
||
if (!state.collaborators.find((u) => u.id === user.id)) {
|
||
state.collaborators.push(user);
|
||
renderCollaborators();
|
||
}
|
||
}
|
||
|
||
function removeCollaborator(userId) {
|
||
state.collaborators = state.collaborators.filter((u) => u.id !== userId);
|
||
renderCollaborators();
|
||
}
|
||
|
||
function renderCollaborators() {
|
||
if (!elements.collaborators) return;
|
||
elements.collaborators.innerHTML = state.collaborators
|
||
.slice(0, 4)
|
||
.map(
|
||
(u) => `
|
||
<div class="collaborator-avatar" style="background:${u.color || "#4285f4"}" title="${escapeHtml(u.name)}">
|
||
${u.name.charAt(0).toUpperCase()}
|
||
</div>
|
||
`,
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
function getUserId() {
|
||
let id = localStorage.getItem("gb-user-id");
|
||
if (!id) {
|
||
id = "user-" + Math.random().toString(36).substr(2, 9);
|
||
localStorage.setItem("gb-user-id", id);
|
||
}
|
||
return id;
|
||
}
|
||
|
||
function getUserName() {
|
||
return localStorage.getItem("gb-user-name") || "Anonymous";
|
||
}
|
||
|
||
function showTransitionsModal() {
|
||
showModal("transitionsModal");
|
||
const currentSlide = state.slides[state.currentSlideIndex];
|
||
if (currentSlide?.transition?.transition_type) {
|
||
selectTransition(currentSlide.transition.transition_type);
|
||
}
|
||
if (currentSlide?.transition?.duration) {
|
||
const durationInput = document.getElementById("transitionDuration");
|
||
const durationValue = document.getElementById("durationValue");
|
||
if (durationInput) durationInput.value = currentSlide.transition.duration;
|
||
if (durationValue)
|
||
durationValue.textContent = `${currentSlide.transition.duration}s`;
|
||
}
|
||
}
|
||
|
||
function selectTransition(transitionType) {
|
||
document.querySelectorAll(".transition-btn").forEach((btn) => {
|
||
btn.classList.toggle("active", btn.dataset.transition === transitionType);
|
||
});
|
||
}
|
||
|
||
function updateDurationDisplay() {
|
||
const durationInput = document.getElementById("transitionDuration");
|
||
const durationValue = document.getElementById("durationValue");
|
||
if (durationInput && durationValue) {
|
||
durationValue.textContent = `${durationInput.value}s`;
|
||
}
|
||
}
|
||
|
||
function applyTransition() {
|
||
const activeBtn = document.querySelector(".transition-btn.active");
|
||
const transitionType = activeBtn?.dataset.transition || "none";
|
||
const duration = parseFloat(
|
||
document.getElementById("transitionDuration")?.value || 0.5,
|
||
);
|
||
const applyToAll = document.getElementById("applyToAllSlides")?.checked;
|
||
|
||
saveToHistory();
|
||
|
||
const transition = {
|
||
transition_type: transitionType,
|
||
duration: duration,
|
||
};
|
||
|
||
if (applyToAll) {
|
||
state.slides.forEach((slide) => {
|
||
slide.transition = { ...transition };
|
||
});
|
||
addChatMessage(
|
||
"assistant",
|
||
`Applied ${transitionType} transition to all slides.`,
|
||
);
|
||
} else {
|
||
const currentSlide = state.slides[state.currentSlideIndex];
|
||
if (currentSlide) {
|
||
currentSlide.transition = transition;
|
||
}
|
||
addChatMessage(
|
||
"assistant",
|
||
`Applied ${transitionType} transition to current slide.`,
|
||
);
|
||
}
|
||
|
||
hideModal("transitionsModal");
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
|
||
function showAnimationsModal() {
|
||
showModal("animationsModal");
|
||
updateSelectedElementInfo();
|
||
updateAnimationOrderList();
|
||
}
|
||
|
||
function updateSelectedElementInfo() {
|
||
const infoEl = document.getElementById("selectedElementInfo");
|
||
if (!infoEl) return;
|
||
|
||
if (state.selectedElement) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const element = slide?.elements?.find(
|
||
(el) => el.id === state.selectedElement,
|
||
);
|
||
if (element) {
|
||
const type = element.element_type || "Unknown";
|
||
const content =
|
||
element.content?.text?.substring(0, 30) ||
|
||
element.content?.shape_type ||
|
||
"";
|
||
infoEl.textContent = `${type}: ${content}${content.length > 30 ? "..." : ""}`;
|
||
return;
|
||
}
|
||
}
|
||
infoEl.textContent = "No element selected";
|
||
}
|
||
|
||
function updateAnimationOrderList() {
|
||
const listEl = document.getElementById("animationOrderList");
|
||
if (!listEl) return;
|
||
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const animations = [];
|
||
|
||
slide?.elements?.forEach((element) => {
|
||
if (element.animations?.length > 0) {
|
||
element.animations.forEach((anim) => {
|
||
animations.push({
|
||
elementId: element.id,
|
||
elementType: element.element_type,
|
||
animation: anim,
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
if (animations.length === 0) {
|
||
listEl.innerHTML = '<p class="no-animations">No animations added yet</p>';
|
||
return;
|
||
}
|
||
|
||
listEl.innerHTML = animations
|
||
.map(
|
||
(item, index) => `
|
||
<div class="animation-item" data-index="${index}">
|
||
<div>
|
||
<div class="animation-name">${item.animation.type || "Animation"}</div>
|
||
<div class="animation-element">${item.elementType}</div>
|
||
</div>
|
||
<button class="animation-remove" data-element="${item.elementId}">×</button>
|
||
</div>
|
||
`,
|
||
)
|
||
.join("");
|
||
|
||
listEl.querySelectorAll(".animation-remove").forEach((btn) => {
|
||
btn.addEventListener("click", () => removeAnimation(btn.dataset.element));
|
||
});
|
||
}
|
||
|
||
function applyAnimation() {
|
||
if (!state.selectedElement) {
|
||
addChatMessage(
|
||
"assistant",
|
||
"Please select an element on the slide first.",
|
||
);
|
||
return;
|
||
}
|
||
|
||
const entrance = document.getElementById("entranceAnimation")?.value;
|
||
const emphasis = document.getElementById("emphasisAnimation")?.value;
|
||
const exit = document.getElementById("exitAnimation")?.value;
|
||
const start =
|
||
document.getElementById("animationStart")?.value || "on-click";
|
||
const duration = parseFloat(
|
||
document.getElementById("animationDuration")?.value || 0.5,
|
||
);
|
||
const delay = parseFloat(
|
||
document.getElementById("animationDelay")?.value || 0,
|
||
);
|
||
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const element = slide?.elements?.find(
|
||
(el) => el.id === state.selectedElement,
|
||
);
|
||
|
||
if (!element) return;
|
||
|
||
saveToHistory();
|
||
|
||
element.animations = [];
|
||
|
||
if (entrance && entrance !== "none") {
|
||
element.animations.push({
|
||
type: entrance,
|
||
category: "entrance",
|
||
start,
|
||
duration,
|
||
delay,
|
||
});
|
||
}
|
||
|
||
if (emphasis && emphasis !== "none") {
|
||
element.animations.push({
|
||
type: emphasis,
|
||
category: "emphasis",
|
||
start: "after-previous",
|
||
duration,
|
||
delay: 0,
|
||
});
|
||
}
|
||
|
||
if (exit && exit !== "none") {
|
||
element.animations.push({
|
||
type: exit,
|
||
category: "exit",
|
||
start: "after-previous",
|
||
duration,
|
||
delay: 0,
|
||
});
|
||
}
|
||
|
||
updateAnimationOrderList();
|
||
hideModal("animationsModal");
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
addChatMessage("assistant", "Animation applied to selected element.");
|
||
}
|
||
|
||
function removeAnimation(elementId) {
|
||
const slide = state.slides[state.currentSlideIndex];
|
||
const element = slide?.elements?.find((el) => el.id === elementId);
|
||
if (element) {
|
||
element.animations = [];
|
||
updateAnimationOrderList();
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
}
|
||
}
|
||
|
||
function previewAnimation() {
|
||
if (!state.selectedElement) {
|
||
addChatMessage(
|
||
"assistant",
|
||
"Select an element to preview its animation.",
|
||
);
|
||
return;
|
||
}
|
||
|
||
const entrance = document.getElementById("entranceAnimation")?.value;
|
||
const node = document.querySelector(
|
||
`[data-element-id="${state.selectedElement}"]`,
|
||
);
|
||
|
||
if (!node || !entrance || entrance === "none") return;
|
||
|
||
node.style.animation = "none";
|
||
node.offsetHeight;
|
||
|
||
const animationName = entrance.replace(/-/g, "");
|
||
node.style.animation = `${animationName} 0.5s ease`;
|
||
|
||
setTimeout(() => {
|
||
node.style.animation = "";
|
||
}, 600);
|
||
}
|
||
|
||
let sorterSlideOrder = [];
|
||
let sorterSelectedSlide = null;
|
||
|
||
function showSlideSorter() {
|
||
showModal("slideSorterModal");
|
||
sorterSlideOrder = state.slides.map((_, i) => i);
|
||
sorterSelectedSlide = null;
|
||
renderSorterGrid();
|
||
}
|
||
|
||
function renderSorterGrid() {
|
||
const grid = document.getElementById("sorterGrid");
|
||
if (!grid) return;
|
||
|
||
grid.innerHTML = sorterSlideOrder
|
||
.map((slideIndex, position) => {
|
||
const slide = state.slides[slideIndex];
|
||
if (!slide) return "";
|
||
|
||
const isSelected = sorterSelectedSlide === position;
|
||
return `
|
||
<div class="sorter-slide ${isSelected ? "selected" : ""}"
|
||
data-position="${position}"
|
||
data-slide-index="${slideIndex}"
|
||
draggable="true">
|
||
<div class="sorter-slide-content">
|
||
${renderSorterSlidePreview(slide)}
|
||
</div>
|
||
<div class="sorter-slide-number">${position + 1}</div>
|
||
<div class="sorter-slide-actions">
|
||
<button data-action="duplicate" title="Duplicate">⎘</button>
|
||
<button data-action="delete" title="Delete">×</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
grid.querySelectorAll(".sorter-slide").forEach((el) => {
|
||
el.addEventListener("click", (e) => {
|
||
if (e.target.closest(".sorter-slide-actions")) return;
|
||
sorterSelectSlide(parseInt(el.dataset.position));
|
||
});
|
||
|
||
el.addEventListener("dragstart", handleSorterDragStart);
|
||
el.addEventListener("dragover", handleSorterDragOver);
|
||
el.addEventListener("drop", handleSorterDrop);
|
||
el.addEventListener("dragend", handleSorterDragEnd);
|
||
|
||
el.querySelectorAll(".sorter-slide-actions button").forEach((btn) => {
|
||
btn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
const action = btn.dataset.action;
|
||
const position = parseInt(el.dataset.position);
|
||
if (action === "duplicate") {
|
||
sorterDuplicateAt(position);
|
||
} else if (action === "delete") {
|
||
sorterDeleteAt(position);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderSorterSlidePreview(slide) {
|
||
const bgColor = slide.background?.color || "#ffffff";
|
||
let html = `<div style="width:100%;height:100%;background:${bgColor};padding:8px;font-size:6px;">`;
|
||
|
||
if (slide.elements) {
|
||
slide.elements.slice(0, 3).forEach((el) => {
|
||
if (el.element_type === "text" && el.content?.text) {
|
||
const text = el.content.text.substring(0, 50);
|
||
html += `<div style="margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(text)}</div>`;
|
||
}
|
||
});
|
||
}
|
||
|
||
html += "</div>";
|
||
return html;
|
||
}
|
||
|
||
function sorterSelectSlide(position) {
|
||
sorterSelectedSlide = position;
|
||
document.querySelectorAll(".sorter-slide").forEach((el) => {
|
||
el.classList.toggle(
|
||
"selected",
|
||
parseInt(el.dataset.position) === position,
|
||
);
|
||
});
|
||
}
|
||
|
||
let draggedPosition = null;
|
||
|
||
function handleSorterDragStart(e) {
|
||
draggedPosition = parseInt(e.currentTarget.dataset.position);
|
||
e.currentTarget.classList.add("dragging");
|
||
e.dataTransfer.effectAllowed = "move";
|
||
}
|
||
|
||
function handleSorterDragOver(e) {
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = "move";
|
||
e.currentTarget.classList.add("drag-over");
|
||
}
|
||
|
||
function handleSorterDrop(e) {
|
||
e.preventDefault();
|
||
const targetPosition = parseInt(e.currentTarget.dataset.position);
|
||
|
||
if (draggedPosition !== null && draggedPosition !== targetPosition) {
|
||
const draggedIndex = sorterSlideOrder[draggedPosition];
|
||
sorterSlideOrder.splice(draggedPosition, 1);
|
||
sorterSlideOrder.splice(targetPosition, 0, draggedIndex);
|
||
renderSorterGrid();
|
||
}
|
||
|
||
e.currentTarget.classList.remove("drag-over");
|
||
}
|
||
|
||
function handleSorterDragEnd(e) {
|
||
e.currentTarget.classList.remove("dragging");
|
||
document.querySelectorAll(".sorter-slide").forEach((el) => {
|
||
el.classList.remove("drag-over");
|
||
});
|
||
draggedPosition = null;
|
||
}
|
||
|
||
function sorterAddSlide() {
|
||
const newSlide = createSlide("blank");
|
||
state.slides.push(newSlide);
|
||
sorterSlideOrder.push(state.slides.length - 1);
|
||
renderSorterGrid();
|
||
}
|
||
|
||
function sorterDuplicateSlide() {
|
||
if (sorterSelectedSlide === null) {
|
||
addChatMessage("assistant", "Select a slide to duplicate.");
|
||
return;
|
||
}
|
||
sorterDuplicateAt(sorterSelectedSlide);
|
||
}
|
||
|
||
function sorterDuplicateAt(position) {
|
||
const originalIndex = sorterSlideOrder[position];
|
||
const original = state.slides[originalIndex];
|
||
if (!original) return;
|
||
|
||
const duplicated = JSON.parse(JSON.stringify(original));
|
||
duplicated.id = generateId();
|
||
state.slides.push(duplicated);
|
||
sorterSlideOrder.splice(position + 1, 0, state.slides.length - 1);
|
||
renderSorterGrid();
|
||
}
|
||
|
||
function sorterDeleteSlide() {
|
||
if (sorterSelectedSlide === null) {
|
||
addChatMessage("assistant", "Select a slide to delete.");
|
||
return;
|
||
}
|
||
sorterDeleteAt(sorterSelectedSlide);
|
||
}
|
||
|
||
function sorterDeleteAt(position) {
|
||
if (sorterSlideOrder.length <= 1) {
|
||
addChatMessage("assistant", "Cannot delete the last slide.");
|
||
return;
|
||
}
|
||
sorterSlideOrder.splice(position, 1);
|
||
if (sorterSelectedSlide >= sorterSlideOrder.length) {
|
||
sorterSelectedSlide = sorterSlideOrder.length - 1;
|
||
}
|
||
renderSorterGrid();
|
||
}
|
||
|
||
function applySorterChanges() {
|
||
const reorderedSlides = sorterSlideOrder.map((i) => state.slides[i]);
|
||
state.slides = reorderedSlides;
|
||
state.currentSlideIndex = 0;
|
||
|
||
hideModal("slideSorterModal");
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
updateSlideCounter();
|
||
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
addChatMessage("assistant", "Slide order updated!");
|
||
}
|
||
|
||
function showExportPdfModal() {
|
||
showModal("exportPdfModal");
|
||
}
|
||
|
||
function exportToPdf() {
|
||
const rangeType = document.querySelector(
|
||
'input[name="slideRange"]:checked',
|
||
)?.value;
|
||
const layout = document.getElementById("pdfLayout")?.value || "full";
|
||
const orientation =
|
||
document.getElementById("pdfOrientation")?.value || "landscape";
|
||
|
||
let slidesToExport = [];
|
||
|
||
switch (rangeType) {
|
||
case "all":
|
||
slidesToExport = state.slides.map((_, i) => i);
|
||
break;
|
||
case "current":
|
||
slidesToExport = [state.currentSlideIndex];
|
||
break;
|
||
case "custom":
|
||
const customRange = document.getElementById("customRange")?.value || "";
|
||
slidesToExport = parseSlideRange(customRange);
|
||
break;
|
||
default:
|
||
slidesToExport = state.slides.map((_, i) => i);
|
||
}
|
||
|
||
if (slidesToExport.length === 0) {
|
||
addChatMessage("assistant", "No slides to export.");
|
||
return;
|
||
}
|
||
|
||
const printWindow = window.open("", "_blank");
|
||
const slidesPerPage = getLayoutSlidesPerPage(layout);
|
||
|
||
let htmlContent = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>${state.presentationName} - PDF Export</title>
|
||
<style>
|
||
@page { size: ${orientation}; margin: 0.5in; }
|
||
@media print {
|
||
.page-break { page-break-after: always; }
|
||
}
|
||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
|
||
.slide-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
padding: 20px;
|
||
}
|
||
.slide {
|
||
background: white;
|
||
border: 1px solid #ccc;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
.slide-full { width: 100%; aspect-ratio: 16/9; }
|
||
.slide-2 { width: 45%; aspect-ratio: 16/9; }
|
||
.slide-4 { width: 45%; aspect-ratio: 16/9; }
|
||
.slide-6 { width: 30%; aspect-ratio: 16/9; }
|
||
.slide-content { padding: 20px; height: 100%; box-sizing: border-box; }
|
||
.slide-number { text-align: center; font-size: 12px; color: #666; margin-top: 8px; }
|
||
.notes-section { padding: 10px; font-size: 11px; border-top: 1px solid #ccc; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
`;
|
||
|
||
let slideCount = 0;
|
||
slidesToExport.forEach((slideIndex, i) => {
|
||
const slide = state.slides[slideIndex];
|
||
if (!slide) return;
|
||
|
||
if (slideCount > 0 && slideCount % slidesPerPage === 0) {
|
||
htmlContent += '<div class="page-break"></div>';
|
||
}
|
||
|
||
if (slideCount % slidesPerPage === 0) {
|
||
htmlContent += '<div class="slide-container">';
|
||
}
|
||
|
||
const slideClass =
|
||
slidesPerPage === 1
|
||
? "slide-full"
|
||
: slidesPerPage === 2
|
||
? "slide-2"
|
||
: slidesPerPage === 4
|
||
? "slide-4"
|
||
: "slide-6";
|
||
const bgColor = slide.background?.color || "#ffffff";
|
||
|
||
htmlContent += `
|
||
<div class="slide ${slideClass}" style="background:${bgColor};">
|
||
<div class="slide-content">
|
||
${renderSlideContentForExport(slide)}
|
||
</div>
|
||
<div class="slide-number">Slide ${slideIndex + 1}</div>
|
||
${layout === "notes" && slide.notes ? `<div class="notes-section">${escapeHtml(slide.notes)}</div>` : ""}
|
||
</div>
|
||
`;
|
||
|
||
slideCount++;
|
||
if (slideCount % slidesPerPage === 0 || i === slidesToExport.length - 1) {
|
||
htmlContent += "</div>";
|
||
}
|
||
});
|
||
|
||
htmlContent += "</body></html>";
|
||
|
||
printWindow.document.write(htmlContent);
|
||
printWindow.document.close();
|
||
printWindow.focus();
|
||
|
||
setTimeout(() => {
|
||
printWindow.print();
|
||
}, 500);
|
||
|
||
hideModal("exportPdfModal");
|
||
addChatMessage(
|
||
"assistant",
|
||
`Exporting ${slidesToExport.length} slide(s) to PDF...`,
|
||
);
|
||
}
|
||
|
||
function parseSlideRange(rangeStr) {
|
||
const slides = [];
|
||
const parts = rangeStr.split(",");
|
||
|
||
parts.forEach((part) => {
|
||
part = part.trim();
|
||
if (part.includes("-")) {
|
||
const [start, end] = part.split("-").map((n) => parseInt(n.trim()) - 1);
|
||
for (
|
||
let i = Math.max(0, start);
|
||
i <= Math.min(state.slides.length - 1, end);
|
||
i++
|
||
) {
|
||
if (!slides.includes(i)) slides.push(i);
|
||
}
|
||
} else {
|
||
const num = parseInt(part) - 1;
|
||
if (num >= 0 && num < state.slides.length && !slides.includes(num)) {
|
||
slides.push(num);
|
||
}
|
||
}
|
||
});
|
||
|
||
return slides.sort((a, b) => a - b);
|
||
}
|
||
|
||
function getLayoutSlidesPerPage(layout) {
|
||
switch (layout) {
|
||
case "full":
|
||
case "notes":
|
||
return 1;
|
||
case "handout-2":
|
||
return 2;
|
||
case "handout-4":
|
||
return 4;
|
||
case "handout-6":
|
||
return 6;
|
||
default:
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
function renderSlideContentForExport(slide) {
|
||
let html = "";
|
||
if (slide.elements) {
|
||
slide.elements.forEach((el) => {
|
||
if (el.element_type === "text" && el.content?.text) {
|
||
const fontSize = el.style?.fontSize || 16;
|
||
const fontWeight = el.style?.fontWeight || "normal";
|
||
const color = el.style?.color || "#000";
|
||
html += `<div style="font-size:${fontSize}px;font-weight:${fontWeight};color:${color};margin-bottom:8px;">${escapeHtml(el.content.text)}</div>`;
|
||
}
|
||
});
|
||
}
|
||
return html || "<p>Empty slide</p>";
|
||
}
|
||
|
||
let selectedMasterLayout = "title";
|
||
|
||
function showMasterSlideModal() {
|
||
showModal("masterSlideModal");
|
||
selectedMasterLayout = "title";
|
||
|
||
if (state.theme) {
|
||
const colors = state.theme.colors || {};
|
||
const fonts = state.theme.fonts || {};
|
||
|
||
setColorInput("masterPrimaryColor", colors.primary || "#4285f4");
|
||
setColorInput("masterSecondaryColor", colors.secondary || "#34a853");
|
||
setColorInput("masterAccentColor", colors.accent || "#fbbc04");
|
||
setColorInput("masterBgColor", colors.background || "#ffffff");
|
||
setColorInput("masterTextColor", colors.text || "#212121");
|
||
setColorInput("masterTextLightColor", colors.text_light || "#666666");
|
||
|
||
setSelectValue("masterHeadingFont", fonts.heading || "Arial");
|
||
setSelectValue("masterBodyFont", fonts.body || "Arial");
|
||
}
|
||
|
||
updateMasterPreview();
|
||
updateMasterLayoutSelection();
|
||
}
|
||
|
||
function setColorInput(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = value;
|
||
}
|
||
|
||
function setSelectValue(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = value;
|
||
}
|
||
|
||
function selectMasterLayout(layout) {
|
||
selectedMasterLayout = layout;
|
||
updateMasterLayoutSelection();
|
||
}
|
||
|
||
function updateMasterLayoutSelection() {
|
||
document.querySelectorAll(".master-layout-item").forEach((item) => {
|
||
item.classList.toggle(
|
||
"active",
|
||
item.dataset.layout === selectedMasterLayout,
|
||
);
|
||
});
|
||
}
|
||
|
||
function updateMasterPreview() {
|
||
const bgColor =
|
||
document.getElementById("masterBgColor")?.value || "#ffffff";
|
||
const textColor =
|
||
document.getElementById("masterTextColor")?.value || "#212121";
|
||
const textLightColor =
|
||
document.getElementById("masterTextLightColor")?.value || "#666666";
|
||
const headingFont =
|
||
document.getElementById("masterHeadingFont")?.value || "Arial";
|
||
const bodyFont =
|
||
document.getElementById("masterBodyFont")?.value || "Arial";
|
||
|
||
const previewSlide = document.querySelector(".preview-slide");
|
||
const previewHeading = document.getElementById("previewHeading");
|
||
const previewBody = document.getElementById("previewBody");
|
||
|
||
if (previewSlide) {
|
||
previewSlide.style.background = bgColor;
|
||
}
|
||
if (previewHeading) {
|
||
previewHeading.style.color = textColor;
|
||
previewHeading.style.fontFamily = headingFont;
|
||
}
|
||
if (previewBody) {
|
||
previewBody.style.color = textLightColor;
|
||
previewBody.style.fontFamily = bodyFont;
|
||
}
|
||
}
|
||
|
||
function applyMasterSlide() {
|
||
const primaryColor =
|
||
document.getElementById("masterPrimaryColor")?.value || "#4285f4";
|
||
const secondaryColor =
|
||
document.getElementById("masterSecondaryColor")?.value || "#34a853";
|
||
const accentColor =
|
||
document.getElementById("masterAccentColor")?.value || "#fbbc04";
|
||
const bgColor =
|
||
document.getElementById("masterBgColor")?.value || "#ffffff";
|
||
const textColor =
|
||
document.getElementById("masterTextColor")?.value || "#212121";
|
||
const textLightColor =
|
||
document.getElementById("masterTextLightColor")?.value || "#666666";
|
||
const headingFont =
|
||
document.getElementById("masterHeadingFont")?.value || "Arial";
|
||
const bodyFont =
|
||
document.getElementById("masterBodyFont")?.value || "Arial";
|
||
|
||
saveToHistory();
|
||
|
||
state.theme = {
|
||
name: "Custom",
|
||
colors: {
|
||
primary: primaryColor,
|
||
secondary: secondaryColor,
|
||
accent: accentColor,
|
||
background: bgColor,
|
||
text: textColor,
|
||
text_light: textLightColor,
|
||
},
|
||
fonts: {
|
||
heading: headingFont,
|
||
body: bodyFont,
|
||
},
|
||
};
|
||
|
||
state.slides.forEach((slide) => {
|
||
slide.background = slide.background || {};
|
||
slide.background.color = bgColor;
|
||
|
||
if (slide.elements) {
|
||
slide.elements.forEach((el) => {
|
||
if (el.element_type === "text") {
|
||
el.style = el.style || {};
|
||
const isHeading =
|
||
el.style.fontSize >= 24 || el.style.fontWeight === "bold";
|
||
el.style.fontFamily = isHeading ? headingFont : bodyFont;
|
||
el.style.color = isHeading ? textColor : textLightColor;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
hideModal("masterSlideModal");
|
||
renderThumbnails();
|
||
renderCurrentSlide();
|
||
|
||
state.isDirty = true;
|
||
scheduleAutoSave();
|
||
addChatMessage("assistant", "Master slide theme applied to all slides!");
|
||
}
|
||
|
||
function resetMasterSlide() {
|
||
setColorInput("masterPrimaryColor", "#4285f4");
|
||
setColorInput("masterSecondaryColor", "#34a853");
|
||
setColorInput("masterAccentColor", "#fbbc04");
|
||
setColorInput("masterBgColor", "#ffffff");
|
||
setColorInput("masterTextColor", "#212121");
|
||
setColorInput("masterTextLightColor", "#666666");
|
||
setSelectValue("masterHeadingFont", "Arial");
|
||
setSelectValue("masterBodyFont", "Arial");
|
||
|
||
updateMasterPreview();
|
||
}
|
||
|
||
window.gbSlides = {
|
||
init,
|
||
addSlide,
|
||
addTextBox,
|
||
addShape,
|
||
addImage,
|
||
duplicateSlide,
|
||
deleteSlide,
|
||
goToSlide,
|
||
startPresentation,
|
||
exitPresentation,
|
||
showModal,
|
||
hideModal,
|
||
toggleChatPanel,
|
||
savePresentation,
|
||
showTransitionsModal,
|
||
showAnimationsModal,
|
||
showSlideSorter,
|
||
exportToPdf,
|
||
showMasterSlideModal,
|
||
};
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|