diff --git a/src/taskpane/components/ProgressBarButtons.tsx b/src/taskpane/components/ProgressBarButtons.tsx index b00dd35f..ff2ad3c3 100644 --- a/src/taskpane/components/ProgressBarButtons.tsx +++ b/src/taskpane/components/ProgressBarButtons.tsx @@ -10,6 +10,28 @@ import { 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", @@ -36,108 +58,96 @@ export const ProgressBarButtons: React.FC = () => { 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 + // Get all slides at once const presentation = context.presentation; const slides = presentation.slides; slides.load("items"); - // Need to load the first slide to get access to dimensions + // Single sync to get all slides await context.sync(); if (slides.items.length === 0) { throw new Error("No slides in the presentation"); } - // Get dimensions from first slide - const firstSlide = slides.items[0]; - - // We'll create our own width and height based on standard values - // since direct access to slide dimensions is tricky in Office.js - const slideWidth = 960; // Standard PowerPoint slide width in points - const slideHeight = 540; // Standard PowerPoint slide height in points - - // Define progress bar properties - const progressBarHeight = 1; // Height in points - const segmentGap = 0; // Gap between segments - const startY = slideHeight - 28; // 28 points from bottom - - // Load slides first to get count - await context.sync(); - + // 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; - // Process each slide + // 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]; - - // Load slide properties to check layout - slide.load("layout"); - await context.sync(); - const layoutName = slide.layout.name; // Check if this is a layout we want to add the progress bar to - // Note: Office.js might not have exact same layout names as VBA, - // so we're checking for common layouts - if (layoutName.includes("Title") || - layoutName.includes("Content") || - layoutName.includes("Two Content") || - layoutName.includes("Blank")) { - - // Add progress bar segments to this slide + 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++) { - // Create line for this segment with a small overlap to prevent gaps - const overlap = 0.5; // Small overlap to ensure no gaps between segments + // Calculate segment position and dimensions const startX = j * segmentWidth; const endX = (j + 1) * segmentWidth; // Create a rectangle with minimal height to simulate a line - // since proper line creation is challenging with the Office JS API - const line = slide.shapes.addGeometricShape("Rectangle"); + const segment = slide.shapes.addGeometricShape("Rectangle"); // Set the rectangle's position to look like a line - // Adding a tiny bit of overlap to ensure no gaps - line.left = startX - (j > 0 ? overlap : 0); // Overlap with previous segment except for first - line.top = startY; - line.width = (endX - startX) + (j > 0 ? overlap : 0) + (j < slideCount - 1 ? overlap : 0); - line.height = progressBarHeight; // Very small height + 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 instead of line color - // We're using a rectangle, so fill is more appropriate than line - if (j <= i) { - // Blue for progressed segments (RGB 32, 81, 112) - line.fill.setSolidColor("#205170"); - - // Make the current slide indicator (last blue segment) slightly more prominent - if (j == i) { - // Make it wider and taller for emphasis - line.width += 1; // Add extra width - } - } else { - // Grey for segments not yet progressed (RGB 242, 242, 242) - line.fill.setSolidColor("#F2F2F2"); - - // Make the current slide indicator (last blue segment) slightly more prominent - if (j == i) { - // Make it wider and taller for emphasis - line.width += 1; // Add extra width - } + // 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 - line.lineFormat.visible = false; + segment.lineFormat.visible = false; - // Since tags API might not be fully supported, use shape name as identifier - line.name = "progress_bar_" + j; + // 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"); }); @@ -150,53 +160,68 @@ export const ProgressBarButtons: React.FC = () => { } }; + /** + * 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 + // Get all slides at once const presentation = context.presentation; const slides = presentation.slides; slides.load("items"); await context.sync(); let removedCount = 0; + const shapesToLoad = []; - // Process each slide + // First, load all shapes from all slides in a batch for (let i = 0; i < slides.items.length; i++) { const slide = slides.items[i]; - - // Load all shapes in this slide const shapes = slide.shapes; shapes.load("items"); - await context.sync(); - - // Find shapes with our tag - const progressBarShapes = []; + 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]; - // Check if this shape name indicates it's a progress bar shape.load("name"); - await context.sync(); - - // Look for shapes with names starting with "progress_bar_" - if (shape.name && shape.name.startsWith("progress_bar_")) { - progressBarShapes.push(shape); - } - } - - // Delete all progress bar shapes - for (const shape of progressBarShapes) { - shape.delete(); - removedCount++; + 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 ${removedCount} progress bar elements from slides.`); - // Added progress bar to slides. setStatusMessage(`Removed progress bar from slides.`); } else { setStatusMessage("No progress bar found to remove."); @@ -240,4 +265,4 @@ export const ProgressBarButtons: React.FC = () => { ); }; -export default ProgressBarButtons; \ No newline at end of file +export default ProgressBarButtons;