puppeteer removed in ipc & used Font displayed
This commit is contained in:
parent
2f432aceea
commit
cc76b9cea4
8 changed files with 297 additions and 403 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import { ipcMain } from "electron";
|
||||
import puppeteer from "puppeteer";
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { baseDir, isDev, tempDir } from "../utils/constants";
|
||||
import { tempDir } from "../utils/constants";
|
||||
interface Position {
|
||||
left: number;
|
||||
top: number;
|
||||
|
|
@ -80,255 +79,271 @@ interface GraphElement {
|
|||
|
||||
type SlideElement = TextElement | PictureElement | BoxElement | LineElement | GraphElement;
|
||||
|
||||
interface SlideMetadata {
|
||||
slideIndex: number;
|
||||
backgroundColor: string;
|
||||
elements: SlideElement[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function setupSlideMetadataHandlers() {
|
||||
ipcMain.handle("get-slide-metadata", async (_, url: string, theme: string, customColors?: any) => {
|
||||
console.log("get-slide-metadata", process.env.NEXT_PUBLIC_FAST_API, url);
|
||||
let browser;
|
||||
let win:BrowserWindow | null = null;
|
||||
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: isDev ? undefined : path.join(baseDir, "dependencies/chrome-headless-shell/linux_build/chrome-headless-shell"),
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
win = new BrowserWindow({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
preload: path.join(__dirname, '../preloads/index.js'),
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
|
||||
// Inject the environment variables before loading the page
|
||||
await page.evaluateOnNewDocument(`
|
||||
window.env = {
|
||||
NEXT_PUBLIC_FAST_API: "${process.env.NEXT_PUBLIC_FAST_API}",
|
||||
};
|
||||
`);
|
||||
|
||||
await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 });
|
||||
|
||||
await page.waitForSelector('[data-element-type="slide-container"]', { timeout: 80000 });
|
||||
|
||||
// Apply theme
|
||||
await page.evaluate((params: any) => {
|
||||
const { theme, customColors } = params;
|
||||
document.querySelectorAll(".slide-theme").forEach((container) => {
|
||||
container.setAttribute("data-theme", theme);
|
||||
await win.loadURL(url,{userAgent:'electron'});
|
||||
|
||||
|
||||
await win.webContents.executeJavaScript(`
|
||||
new Promise((resolve) => {
|
||||
const check = () => {
|
||||
const el = document.querySelector('[data-element-type="slide-container"]');
|
||||
if (el) return resolve(true);
|
||||
setTimeout(check, 200);
|
||||
};
|
||||
check();
|
||||
});
|
||||
`);
|
||||
const metadata = await win.webContents.executeJavaScript(
|
||||
`
|
||||
(() => {
|
||||
const rgbToHex = (color) => {
|
||||
if (!color || color === "transparent" || color === "none") return "000000";
|
||||
if (color.startsWith("#")) return color.replace("#", "");
|
||||
const matches = color.match(/\\d+/g);
|
||||
if (!matches) return "000000";
|
||||
const [r, g, b] = matches.map(x => parseInt(x));
|
||||
return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
|
||||
if (theme === "custom" && customColors) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--custom-slide-bg", customColors.slideBg);
|
||||
root.style.setProperty("--custom-slide-title", customColors.slideTitle);
|
||||
root.style.setProperty("--custom-slide-heading", customColors.slideHeading);
|
||||
root.style.setProperty("--custom-slide-description", customColors.slideDescription);
|
||||
root.style.setProperty("--custom-slide-box", customColors.slideBox);
|
||||
}
|
||||
}, { theme, customColors });
|
||||
const slidesMetadata = [];
|
||||
const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]');
|
||||
|
||||
// Get slide metadata
|
||||
const metadata = await page.evaluate(() => {
|
||||
const rgbToHex = (color: string): string => {
|
||||
if (!color || color === "transparent" || color === "none") return "000000";
|
||||
if (color.startsWith("#")) return color.replace("#", "");
|
||||
const matches = color.match(/\d+/g);
|
||||
if (!matches) return "000000";
|
||||
const [r, g, b] = matches.map(x => parseInt(x));
|
||||
return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
slideContainers.forEach((container) => {
|
||||
const containerEl = container;
|
||||
containerEl.style.width = "1280px";
|
||||
containerEl.style.height = "720px";
|
||||
containerEl.style.transform = "none";
|
||||
|
||||
const slidesMetadata: SlideMetadata[] = [];
|
||||
const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]');
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0");
|
||||
const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor);
|
||||
|
||||
slideContainers.forEach((container) => {
|
||||
const containerEl = container as HTMLElement;
|
||||
containerEl.style.width = "1280px";
|
||||
containerEl.style.height = "720px";
|
||||
containerEl.style.transform = "none";
|
||||
const elements = [];
|
||||
const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])');
|
||||
|
||||
const containerRect = containerEl.getBoundingClientRect();
|
||||
const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0");
|
||||
const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor);
|
||||
slideElements.forEach((element) => {
|
||||
const el = element;
|
||||
const elementRect = el.getBoundingClientRect();
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
|
||||
const elements: SlideElement[] = [];
|
||||
const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])');
|
||||
const position = {
|
||||
left: Math.round(elementRect.left - containerRect.left),
|
||||
top: Math.round(elementRect.top - containerRect.top),
|
||||
width: Math.round(elementRect.width),
|
||||
height: Math.round(elementRect.height),
|
||||
};
|
||||
|
||||
slideElements.forEach((element) => {
|
||||
const el = element as HTMLElement;
|
||||
const elementRect = el.getBoundingClientRect();
|
||||
const computedStyle = window.getComputedStyle(el);
|
||||
const elementType = el.getAttribute("data-element-type");
|
||||
if (!elementType) return;
|
||||
|
||||
const position: Position = {
|
||||
left: Math.round(elementRect.left - containerRect.left),
|
||||
top: Math.round(elementRect.top - containerRect.top),
|
||||
width: Math.round(elementRect.width),
|
||||
height: Math.round(elementRect.height),
|
||||
};
|
||||
switch (elementType) {
|
||||
case "text":
|
||||
elements.push({
|
||||
position,
|
||||
paragraphs: [{
|
||||
alignment: el.getAttribute("data-is-align") === 'true' ? 2 : 1,
|
||||
text: el.getAttribute("data-text-content") || el.textContent || "",
|
||||
font: {
|
||||
name: computedStyle.fontFamily.split('_')[2] || 'Inter',
|
||||
size: parseInt(computedStyle.fontSize),
|
||||
bold: parseInt(computedStyle.fontWeight) >= 500,
|
||||
weight: parseInt(computedStyle.fontWeight),
|
||||
color: rgbToHex(computedStyle.color),
|
||||
},
|
||||
}],
|
||||
});
|
||||
break;
|
||||
|
||||
const elementType = el.getAttribute("data-element-type");
|
||||
if (!elementType) return;
|
||||
case "picture":
|
||||
const imgEl = el.tagName.toLowerCase() === "img" ? el : el.querySelector("img");
|
||||
if (imgEl) {
|
||||
elements.push({
|
||||
position,
|
||||
picture: {
|
||||
is_network: imgEl.src.startsWith("http"),
|
||||
path: imgEl.src || imgEl.getAttribute("data-image-path") || "",
|
||||
},
|
||||
shape: imgEl.getAttribute('data-image-type'),
|
||||
object_fit: {
|
||||
fit: imgEl.getAttribute('data-object-fit'),
|
||||
focus: [
|
||||
parseFloat(imgEl.getAttribute('data-focial-point-x') || '0'),
|
||||
parseFloat(imgEl.getAttribute('data-focial-point-y') || '0'),
|
||||
],
|
||||
},
|
||||
overlay: el.getAttribute("data-is-icon") ? "ffffff" : null,
|
||||
border_radius: Array(4).fill(parseInt(computedStyle.borderRadius) || 0),
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
switch (elementType) {
|
||||
case "text":
|
||||
elements.push({
|
||||
position,
|
||||
paragraphs: [{
|
||||
alignment: el.getAttribute("data-is-align") === 'true' ? 2 : 1,
|
||||
text: el.getAttribute("data-text-content") || el.textContent || "",
|
||||
font: {
|
||||
name: computedStyle.fontFamily.split('_')[2] || 'Inter',
|
||||
size: parseInt(computedStyle.fontSize),
|
||||
bold: parseInt(computedStyle.fontWeight) >= 500,
|
||||
weight: parseInt(computedStyle.fontWeight),
|
||||
color: rgbToHex(computedStyle.color),
|
||||
},
|
||||
}],
|
||||
} as TextElement);
|
||||
break;
|
||||
|
||||
case "picture":
|
||||
const imgEl = el.tagName.toLowerCase() === "img" ? el as HTMLImageElement : el.querySelector("img") as HTMLImageElement;
|
||||
if (imgEl) {
|
||||
case "graph":
|
||||
elements.push({
|
||||
position,
|
||||
picture: {
|
||||
is_network: imgEl.src.startsWith("http"),
|
||||
path: imgEl.src || imgEl.getAttribute("data-image-path") || "",
|
||||
is_network: true,
|
||||
path: \`__GRAPH_PLACEHOLDER__\${el.getAttribute("data-element-id")}\`,
|
||||
},
|
||||
shape: imgEl.getAttribute('data-image-type'),
|
||||
object_fit: {
|
||||
fit: imgEl.getAttribute('data-object-fit'),
|
||||
focus: [
|
||||
parseFloat(imgEl.getAttribute('data-focial-point-x') || '0'),
|
||||
parseFloat(imgEl.getAttribute('data-focial-point-y') || '0'),
|
||||
],
|
||||
},
|
||||
overlay: el.getAttribute("data-is-icon") ? "ffffff" : null,
|
||||
border_radius: Array(4).fill(parseInt(computedStyle.borderRadius) || 0),
|
||||
} as PictureElement);
|
||||
}
|
||||
break;
|
||||
border_radius: [0, 0, 0, 0],
|
||||
});
|
||||
break;
|
||||
|
||||
case "graph":
|
||||
elements.push({
|
||||
position,
|
||||
picture: {
|
||||
is_network: true,
|
||||
path: `__GRAPH_PLACEHOLDER__${el.getAttribute("data-element-id")}`,
|
||||
},
|
||||
border_radius: [0, 0, 0, 0],
|
||||
} as GraphElement);
|
||||
break;
|
||||
case "slide-box":
|
||||
case "filledbox":
|
||||
const boxShadow = computedStyle.boxShadow;
|
||||
let shadowRadius = 0;
|
||||
let shadowColor = "000000";
|
||||
let shadowOffsetX = 0;
|
||||
let shadowOffsetY = 0;
|
||||
let shadowOpacity = 0;
|
||||
|
||||
case "slide-box":
|
||||
case "filledbox":
|
||||
const boxShadow = computedStyle.boxShadow;
|
||||
let shadowRadius = 0;
|
||||
let shadowColor = "000000";
|
||||
let shadowOffsetX = 0;
|
||||
let shadowOffsetY = 0;
|
||||
let shadowOpacity = 0;
|
||||
if (boxShadow && boxShadow !== "none") {
|
||||
const boxShadowRegex =
|
||||
/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+),?\\s*([\\d.]+)?\\)?\\s+(-?\\d+)px\\s+(-?\\d+)px\\s+(-?\\d+)px/;
|
||||
const match = boxShadow.match(boxShadowRegex);
|
||||
|
||||
if (boxShadow && boxShadow !== "none") {
|
||||
const boxShadowRegex =
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)?\s+(-?\d+)px\s+(-?\d+)px\s+(-?\d+)px/;
|
||||
const match = boxShadow.match(boxShadowRegex);
|
||||
|
||||
if (match) {
|
||||
const r = match[1];
|
||||
const g = match[2];
|
||||
const b = match[3];
|
||||
const rgbStr = "rgb(" + r + ", " + g + ", " + b + ")";
|
||||
shadowColor = rgbToHex(rgbStr);
|
||||
shadowOpacity = match[4] ? parseFloat(match[4]) : 1;
|
||||
shadowOffsetX = parseInt(match[5]);
|
||||
shadowOffsetY = parseInt(match[6]);
|
||||
shadowRadius = parseInt(match[7]);
|
||||
if (match) {
|
||||
const r = match[1];
|
||||
const g = match[2];
|
||||
const b = match[3];
|
||||
const rgbStr = "rgb(" + r + ", " + g + ", " + b + ")";
|
||||
shadowColor = rgbToHex(rgbStr);
|
||||
shadowOpacity = match[4] ? parseFloat(match[4]) : 1;
|
||||
shadowOffsetX = parseInt(match[5]);
|
||||
shadowOffsetY = parseInt(match[6]);
|
||||
shadowRadius = parseInt(match[7]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elements.push({
|
||||
position,
|
||||
type:
|
||||
computedStyle.borderRadius === "9999px" ||
|
||||
computedStyle.borderRadius === "50%"
|
||||
? 9
|
||||
: 5,
|
||||
fill: {
|
||||
color: rgbToHex(computedStyle.backgroundColor),
|
||||
},
|
||||
border_radius: parseInt(computedStyle.borderRadius) || 0,
|
||||
stroke: {
|
||||
color: rgbToHex(computedStyle.borderColor),
|
||||
thickness: parseInt(computedStyle.borderWidth) || 0,
|
||||
},
|
||||
shadow: {
|
||||
radius: shadowRadius,
|
||||
color: shadowColor,
|
||||
offset: Math.sqrt(
|
||||
shadowOffsetX * shadowOffsetX +
|
||||
shadowOffsetY * shadowOffsetY
|
||||
),
|
||||
opacity: shadowOpacity,
|
||||
angle: Math.round(
|
||||
(Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI
|
||||
),
|
||||
},
|
||||
});
|
||||
break;
|
||||
elements.push({
|
||||
position,
|
||||
type:
|
||||
computedStyle.borderRadius === "9999px" ||
|
||||
computedStyle.borderRadius === "50%"
|
||||
? 9
|
||||
: 5,
|
||||
fill: {
|
||||
color: rgbToHex(computedStyle.backgroundColor),
|
||||
},
|
||||
border_radius: parseInt(computedStyle.borderRadius) || 0,
|
||||
stroke: {
|
||||
color: rgbToHex(computedStyle.borderColor),
|
||||
thickness: parseInt(computedStyle.borderWidth) || 0,
|
||||
},
|
||||
shadow: {
|
||||
radius: shadowRadius,
|
||||
color: shadowColor,
|
||||
offset: Math.sqrt(
|
||||
shadowOffsetX * shadowOffsetX +
|
||||
shadowOffsetY * shadowOffsetY
|
||||
),
|
||||
opacity: shadowOpacity,
|
||||
angle: Math.round(
|
||||
(Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI
|
||||
),
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case "line":
|
||||
elements.push({
|
||||
position,
|
||||
lineType: 1,
|
||||
thickness: computedStyle.borderWidth || computedStyle.height,
|
||||
color: rgbToHex(
|
||||
computedStyle.borderColor || computedStyle.backgroundColor
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
elements.push({
|
||||
position,
|
||||
lineType: 1,
|
||||
thickness: computedStyle.borderWidth || computedStyle.height,
|
||||
color: rgbToHex(
|
||||
computedStyle.borderColor || computedStyle.backgroundColor
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
slidesMetadata.push({ slideIndex, backgroundColor, elements });
|
||||
});
|
||||
|
||||
slidesMetadata.push({ slideIndex, backgroundColor, elements });
|
||||
});
|
||||
return slidesMetadata;
|
||||
})();
|
||||
`
|
||||
)
|
||||
// ✅ Handle Graphs: capture each graph element as an image
|
||||
const graphIds: { id: string; bounds: Electron.Rectangle }[] = await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
return Array.from(document.querySelectorAll('[data-element-type="graph"]')).map(el => el.getAttribute("data-element-id"));
|
||||
})();
|
||||
`);
|
||||
|
||||
for (const id of graphIds) {
|
||||
try {
|
||||
// Scroll into view first
|
||||
await win.webContents.executeJavaScript(`
|
||||
document.querySelector('[data-element-id="${id}"]').scrollIntoView({ behavior: 'instant', block: 'center' });
|
||||
|
||||
`);
|
||||
// Wait a bit for any animations/rendering to complete
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const bounds: Electron.Rectangle = await win.webContents.executeJavaScript(`
|
||||
(() => {
|
||||
const el = document.querySelector('[data-element-id="${id}"]');
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(rect.left),
|
||||
y: Math.round(rect.top),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
};
|
||||
})();
|
||||
`);
|
||||
|
||||
return slidesMetadata;
|
||||
});
|
||||
|
||||
// Handle graph elements
|
||||
const graphElements = await page.$$('[data-element-type="graph"]');
|
||||
for (const graphElement of graphElements) {
|
||||
const graphId = await graphElement.evaluate(el => el.getAttribute("data-element-id"));
|
||||
const screenshot = await graphElement.screenshot({
|
||||
type: "jpeg",
|
||||
encoding: "base64",
|
||||
quality: 100,
|
||||
omitBackground: true,
|
||||
});
|
||||
|
||||
|
||||
const filename = `chart-${graphId}-${Date.now()}.jpg`;
|
||||
const filePath = path.join(tempDir, filename);
|
||||
fs.writeFileSync(filePath, Buffer.from(screenshot, 'base64'));
|
||||
|
||||
metadata.forEach(slide => {
|
||||
slide.elements.forEach(element => {
|
||||
if ('picture' in element && element.picture.path === `__GRAPH_PLACEHOLDER__${graphId}`) {
|
||||
const image = await win.webContents.capturePage(bounds);
|
||||
const buffer = image.toJPEG(100);
|
||||
|
||||
|
||||
if (buffer.length === 0) {
|
||||
console.error("Empty buffer! Graph not captured.");
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(tempDir, `chart-${id}-${Date.now()}.jpeg`);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
|
||||
// Update metadata
|
||||
metadata.forEach((slide: any) => {
|
||||
slide.elements.forEach((element: any) => {
|
||||
if ("picture" in element && element.picture.path === `__GRAPH_PLACEHOLDER__${id}`) {
|
||||
element.picture.path = filePath;
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Failed to capture or save chart-${id}:`, err);
|
||||
}
|
||||
|
||||
}
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
console.error("Error during page preparation:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
// if (browser) await browser.close();
|
||||
if (win) win.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,24 +30,25 @@ const AllChart = ({
|
|||
const slide =
|
||||
state.presentationGeneration?.presentationData?.slides[slideIndex];
|
||||
|
||||
const style = slide?.content.graph.style;
|
||||
|
||||
const style = slide?.content.graph.style || {};
|
||||
return Object.keys(
|
||||
style === null || style === undefined ? {} : (style as ChartSettings)
|
||||
).length > 0
|
||||
? (style as ChartSettings)
|
||||
: {
|
||||
showLegend: false,
|
||||
showGrid: false,
|
||||
showAxisLabel: true,
|
||||
showDataLabel: true,
|
||||
dataLabel: {
|
||||
dataLabelPosition:
|
||||
slide?.content.graph.type === "pie"
|
||||
? ("Outside" as const)
|
||||
: ("Inside" as const),
|
||||
dataLabelAlignment: "Center" as const,
|
||||
},
|
||||
};
|
||||
showLegend: false,
|
||||
showGrid: false,
|
||||
showAxisLabel: true,
|
||||
showDataLabel: true,
|
||||
dataLabel: {
|
||||
dataLabelPosition:
|
||||
slide?.content.graph.type === "pie"
|
||||
? ("Outside" as const)
|
||||
: ("Inside" as const),
|
||||
dataLabelAlignment: "Center" as const,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -140,175 +140,7 @@ const LayoutPreview = ({ type }: { type: string }) => {
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type10':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="flex justify-center gap-8 flex-1">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="text-center flex flex-col items-center">
|
||||
<div className="w-12 h-12 rounded-full border-4 border-blue-200 flex items-center justify-center">
|
||||
|
||||
<div className="text-xs text-gray-500">{i * 10}%</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 w-24 mt-2"></div>
|
||||
<div className="h-1.5 bg-gray-100 w-20 mt-1"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type11':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="flex justify-between gap-4 flex-1">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex-1 bg-gray-50 p-2 rounded flex flex-col items-center">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-200 flex items-center justify-center mb-2">
|
||||
<div className="text-xs text-gray-500">257</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 w-3/4 mb-1"></div>
|
||||
<div className="h-1.5 bg-gray-100 w-2/3"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type12':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex justify-between gap-4">
|
||||
<div className="w-1/2 flex flex-col justify-center space-y-2">
|
||||
<div className="h-3 bg-gray-200 w-3/4"></div>
|
||||
<div className="h-2 bg-gray-100 w-full"></div>
|
||||
</div>
|
||||
<div className="w-1/2 flex justify-center items-center">
|
||||
<div className="w-16 h-16 rounded-full border-4 border-t-blue-200 border-r-blue-200 border-b-gray-50 border-l-gray-50 flex items-center justify-center">
|
||||
<div className="w-4 h-4 text-xs ">Icon</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type13':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="text-center space-y-1">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="h-2 bg-gray-100 w-3/4 mx-auto"></div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-12 flex-1">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="text-center flex flex-col items-center">
|
||||
<div className="w-12 h-12 rounded-full border-4 border-t-blue-200 border-r-blue-200 border-b-gray-50 border-l-gray-50 flex items-center justify-center mb-1">
|
||||
<div className="text-xs ">Icon</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 w-32 mb-1"></div>
|
||||
<div className="h-1.5 bg-gray-100 w-24"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type14':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="text-center space-y-1">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="h-2 bg-gray-100 w-3/4 mx-auto"></div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-6 flex-1">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex-1 bg-gray-50 p-2 rounded flex flex-col items-center">
|
||||
<div className="relative w-16 h-10">
|
||||
<svg width="64" height="40" viewBox="0 0 64 40">
|
||||
<path
|
||||
d="M 4,36 A 28,28 0 0 1 60,36"
|
||||
fill="none"
|
||||
stroke="#4895ef"
|
||||
strokeWidth="6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<text
|
||||
x="32"
|
||||
y="28"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
fontSize="12"
|
||||
fontWeight="500"
|
||||
fill="#4895ef"
|
||||
>
|
||||
{i === 1 ? '30%' : '80%'}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 w-3/4 mb-1 mt-2"></div>
|
||||
<div className="h-1.5 bg-gray-100 w-2/3 text-center"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type15':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="text-center space-y-1">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="h-2 bg-gray-100 w-3/4 mx-auto"></div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-6 flex-1">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex-1 bg-white shadow-sm rounded-lg p-2 flex flex-col items-center">
|
||||
<div className="relative w-16 h-10">
|
||||
<svg width="64" height="40" viewBox="0 0 64 40">
|
||||
<path
|
||||
d="M 4,36 A 28,28 0 0 1 60,36"
|
||||
fill="none"
|
||||
stroke="#4895ef"
|
||||
strokeWidth="6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<g transform={`rotate(${i === 1 ? '-60' : '-20'} 32 36)`}>
|
||||
<line
|
||||
x1="32"
|
||||
y1="36"
|
||||
x2="32"
|
||||
y2="14"
|
||||
stroke="#4895ef"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 w-3/4 mb-1 mt-2"></div>
|
||||
<div className="h-1.5 bg-gray-100 w-2/3 text-center"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'type16':
|
||||
return (
|
||||
<div className="w-full h-[120px] bg-white p-3 flex flex-col gap-2">
|
||||
<div className="text-center space-y-1">
|
||||
<div className="h-3 bg-gray-200 w-1/2 mx-auto"></div>
|
||||
<div className="h-2 bg-gray-100 w-3/4 mx-auto"></div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-6 flex-1">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex-1 bg-white shadow-sm rounded-lg p-3 flex flex-col">
|
||||
<div className="h-2 w-full bg-gray-100 rounded-full mb-2">
|
||||
<div
|
||||
className={`h-full ${i === 1 ? 'w-2/3' : 'w-3/5'} bg-blue-300 rounded-full`}
|
||||
></div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 w-3/4 mb-1"></div>
|
||||
<div className="h-1.5 bg-gray-100 w-full text-center"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
@ -323,7 +155,7 @@ const NewSlide = ({ onSelectLayout, setShowNewSlideSelection }: NewSlideProps) =
|
|||
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
|
||||
</div>
|
||||
<div className='grid grid-cols-4 gap-4'>
|
||||
{['type1', 'type2', 'type4', 'type5', 'type6', 'type7', 'type8', 'type9', 'type10', 'type11', 'type12', 'type13', 'type14', 'type15', 'type16'].map((type) => (
|
||||
{['type1', 'type2', 'type4', 'type5', 'type6', 'type7', 'type8', 'type9'].map((type) => (
|
||||
<div
|
||||
key={type}
|
||||
className="transform hover:scale-105 transition-transform cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
SquareArrowOutUpRight,
|
||||
Play,
|
||||
Loader2,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
|
|
@ -45,6 +46,7 @@ import ThemeSelector from "./ThemeSelector";
|
|||
import Modal from "./Modal";
|
||||
|
||||
import Announcement from "@/components/Announcement";
|
||||
import { getFontLink } from "../../utils/others";
|
||||
|
||||
const Header = ({
|
||||
presentation_id,
|
||||
|
|
@ -125,9 +127,8 @@ const Header = ({
|
|||
const getSlideMetadata = async () => {
|
||||
try {
|
||||
// Get the current URL without any query parameters
|
||||
// const baseUrl = `${window.location.origin}/pdf-maker?id=${presentation_id}`;
|
||||
const baseUrl = window.location.href;
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const metadata = await window.electron.getSlideMetadata(
|
||||
baseUrl,
|
||||
|
|
@ -258,6 +259,12 @@ const Header = ({
|
|||
<img src="/pptx.svg" alt="pptx export" width={30} height={30} />
|
||||
Export as PPTX
|
||||
</Button>
|
||||
<p className="text-sm pt-3 border-t border-gray-300">
|
||||
Font Used:
|
||||
<a className="text-blue-500 flex items-center gap-1" href={getFontLink(currentColors.fontFamily).link || ''} target="_blank" rel="noopener noreferrer">
|
||||
{getFontLink(currentColors.fontFamily).name || ''} <ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -320,9 +327,7 @@ const Header = ({
|
|||
{isStreaming && (
|
||||
<Loader2 className="animate-spin text-white font-bold w-6 h-6" />
|
||||
)}
|
||||
<button onClick={() => router.push(`/pdf-maker?id=${presentation_id}`)}>
|
||||
go there
|
||||
</button>
|
||||
|
||||
<Select value={currentTheme} onValueChange={handleThemeSelect}>
|
||||
<SelectTrigger className="w-[160px] bg-[#6358fd] text-white border-none hover:bg-[#5146E5] transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
console.error("Error AAYO", error);
|
||||
})
|
||||
.finally(() => {
|
||||
console.log("Auto finished");
|
||||
setAutoSaveLoading(false);
|
||||
});
|
||||
},
|
||||
|
|
@ -114,6 +115,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
(slide) => slide.images && slide.images.length > 0
|
||||
)
|
||||
) {
|
||||
|
||||
debouncedSave({
|
||||
presentation_id: presentation_id,
|
||||
slides: presentationData.slides,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ const SlideContent = ({
|
|||
);
|
||||
|
||||
if (response) {
|
||||
console.log("response", response);
|
||||
dispatch(updateSlide({ index: slide.index, slide: response }));
|
||||
toast({
|
||||
title: "Success",
|
||||
|
|
@ -196,9 +197,8 @@ const SlideContent = ({
|
|||
<button
|
||||
disabled={isUpdating}
|
||||
type="submit"
|
||||
className={`bg-gradient-to-r from-[#9034EA] to-[#5146E5] rounded-[32px] px-4 py-2 text-white flex items-center justify-end gap-2 ml-auto ${
|
||||
isUpdating ? "opacity-70 cursor-not-allowed" : ""
|
||||
}`}
|
||||
className={`bg-gradient-to-r from-[#9034EA] to-[#5146E5] rounded-[32px] px-4 py-2 text-white flex items-center justify-end gap-2 ml-auto ${isUpdating ? "opacity-70 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
{isUpdating ? "Updating..." : "Update"}
|
||||
<SendHorizontal className="w-4 sm:w-5 h-4 sm:h-5" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { randomChartGenerator } from "@/lib/utils";
|
||||
import { Slide } from "../types/slide";
|
||||
import { generateRandomId } from "./others";
|
||||
|
||||
const randomGraph = (presentation_id: string) => {
|
||||
const randomData = randomChartGenerator();
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
id: generateRandomId(),
|
||||
name: "Sales Performance",
|
||||
type: "bar",
|
||||
presentation: presentation_id,
|
||||
|
|
@ -20,7 +21,7 @@ export const getEmptySlideContent = (
|
|||
presentation_id: string
|
||||
): Slide => {
|
||||
const baseSlide: Slide = {
|
||||
id: crypto.randomUUID(),
|
||||
id: generateRandomId(),
|
||||
type,
|
||||
index,
|
||||
design_index: 1,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,44 @@ export function removeUUID(fileName: string) {
|
|||
return fileName; // In case there's no extension
|
||||
}
|
||||
|
||||
|
||||
export function generateRandomId(): string {
|
||||
const length = 36;
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
|
||||
let id = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * chars.length);
|
||||
id += chars[randomIndex];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export const getFontLink = (fontName: string) => {
|
||||
if (!fontName) {
|
||||
return {link: '', name: ''};
|
||||
}
|
||||
|
||||
if ( fontName.includes('instrument')) {
|
||||
return{link: 'https://fonts.google.com/specimen/Instrument+Sans', name: 'Instrument Sans'}
|
||||
}
|
||||
if (fontName.includes('fraunces')) {
|
||||
return{link: 'https://fonts.google.com/specimen/Fraunces', name: 'Fraunces'}
|
||||
}
|
||||
if (fontName.includes('montserrat')) {
|
||||
return{link: 'https://fonts.google.com/specimen/Montserrat', name: 'Montserrat'}
|
||||
}
|
||||
if (fontName.includes('inria-serif')) {
|
||||
return{link: 'https://fonts.google.com/specimen/Inria+Serif', name: 'Inria Serif'}
|
||||
}
|
||||
if(fontName.includes('inter')) {
|
||||
return{link: 'https://fonts.google.com/specimen/Inter', name: 'Inter'}
|
||||
}
|
||||
else {
|
||||
return {link: '', name: ''};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const numberTranslations: any = {
|
||||
// Key languages
|
||||
"English (English)": ["01", "02", "03", "04", "05"],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue