/** * @file ProgressBarButtons.tsx * @description Component that provides functionality to add and remove progress bars * on PowerPoint slides. The progress bars visually indicate the current position within * the presentation, with colored segments showing completed and upcoming slides. */ import * as React from "react"; import { Button, makeStyles } from "@fluentui/react-components"; import { GaugeRegular, DismissRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; // Constants for progress bar configuration const PROGRESS_BAR_CONFIG = { // Standard PowerPoint slide dimensions in points slideWidth: 960, slideHeight: 540, // Progress bar appearance barHeight: 1, distanceFromBottom: 28, overlap: 0.5, // Small overlap to ensure no gaps between segments // Colors completedColor: "#205170", // Blue for progressed segments (RGB 32, 81, 112) pendingColor: "#F2F2F2" // Grey for segments not yet progressed (RGB 242, 242, 242) }; // Layouts that should receive progress bars const SUPPORTED_LAYOUTS = [ "Title", "Content", "Two Content", "Blank" ]; const useStyles = makeStyles({ buttonGrid: { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", width: "100%" }, progressButton: { width: "100%", minWidth: 0, padding: "8px 4px", "& span": { justifyContent: "center" } } }); export const ProgressBarButtons: React.FC = () => { const styles = useStyles(); const commonStyles = useCommonStyles(); const { statusMessage, setStatusMessage, statusType, setStatusType, isProcessing, setIsProcessing } = useStatusContext(); /** * Adds a progress bar to the bottom of each slide in the presentation * The progress bar shows the current position within the presentation * with colored segments indicating progress */ const addProgressBar = async () => { setIsProcessing(true); const startTime = performance.now(); try { await PowerPoint.run(async (context) => { // Get all slides at once const presentation = context.presentation; const slides = presentation.slides; slides.load("items"); // Single sync to get all slides await context.sync(); if (slides.items.length === 0) { throw new Error("No slides in the presentation"); } // Calculate dimensions based on slide count const { slideWidth, slideHeight, barHeight, distanceFromBottom, overlap } = PROGRESS_BAR_CONFIG; const slideCount = slides.items.length; const segmentWidth = slideWidth / slideCount; const startY = slideHeight - distanceFromBottom; // Load all slide layouts at once to reduce API calls for (let i = 0; i < slideCount; i++) { slides.items[i].load("layout"); } // Single sync to get all layouts await context.sync(); // Process each slide without unnecessary syncs for (let i = 0; i < slideCount; i++) { const slide = slides.items[i]; const layoutName = slide.layout.name; // Check if this is a layout we want to add the progress bar to const isCompatibleLayout = SUPPORTED_LAYOUTS.some(layout => layoutName.includes(layout) ); if (isCompatibleLayout) { // Create all segments for this slide in a batch for (let j = 0; j < slideCount; j++) { // Calculate segment position and dimensions const startX = j * segmentWidth; const endX = (j + 1) * segmentWidth; // Create a rectangle with minimal height to simulate a line const segment = slide.shapes.addGeometricShape("Rectangle"); // Set the rectangle's position to look like a line segment.left = startX - (j > 0 ? overlap : 0); segment.top = startY; segment.width = (endX - startX) + (j > 0 ? overlap : 0) + (j < slideCount - 1 ? overlap : 0); segment.height = barHeight; // Set fill color based on progress const isCompleted = j <= i; segment.fill.setSolidColor(isCompleted ? PROGRESS_BAR_CONFIG.completedColor : PROGRESS_BAR_CONFIG.pendingColor ); // Make the current slide indicator slightly more prominent if (j == i) { segment.width += 1; } // Remove the border/outline entirely segment.lineFormat.visible = false; // Use shape name as identifier for later removal segment.name = "progress_bar_" + j; } } } // Single final sync to commit all changes await context.sync(); const endTime = performance.now(); console.log(`Progress bar creation took ${endTime - startTime}ms`); setStatusMessage("Added progress bar to slides."); setStatusType("success"); }); } catch (error) { setStatusMessage(`Error: ${error.message}`); setStatusType("error"); console.error("Add progress bar error:", error); } finally { setIsProcessing(false); } }; /** * Removes all progress bar elements from all slides in the presentation * Identifies progress bar elements by their name prefix "progress_bar_" */ const removeProgressBar = async () => { setIsProcessing(true); const startTime = performance.now(); try { await PowerPoint.run(async (context) => { // Get all slides at once const presentation = context.presentation; const slides = presentation.slides; slides.load("items"); await context.sync(); let removedCount = 0; const shapesToLoad = []; // First, load all shapes from all slides in a batch for (let i = 0; i < slides.items.length; i++) { const slide = slides.items[i]; const shapes = slide.shapes; shapes.load("items"); shapesToLoad.push({ slide, shapes }); } // Single sync to get all shapes await context.sync(); // Now load all shape names in a batch const allShapesWithNames = []; for (const { slide, shapes } of shapesToLoad) { for (let j = 0; j < shapes.items.length; j++) { const shape = shapes.items[j]; shape.load("name"); allShapesWithNames.push({ slide, shape }); } } // Single sync to get all shape names await context.sync(); // Filter shapes that are progress bars and delete them const progressBarShapes = allShapesWithNames.filter( ({ shape }) => shape.name && shape.name.startsWith("progress_bar_") ); // Delete all progress bar shapes for (const { shape } of progressBarShapes) { shape.delete(); removedCount++; } // Single final sync to commit all deletions await context.sync(); const endTime = performance.now(); console.log(`Progress bar removal took ${endTime - startTime}ms`); if (removedCount > 0) { setStatusMessage(`Removed progress bar from slides.`); } else { setStatusMessage("No progress bar found to remove."); } setStatusType("success"); }); } catch (error) { setStatusMessage(`Error: ${error.message}`); setStatusType("error"); console.error("Remove progress bar error:", error); } finally { setIsProcessing(false); } }; return (