/** * @file DraftButtons.tsx * @description Component that provides functionality to add and remove draft watermarks * on PowerPoint master slides. The watermarks appear as large, light-colored text * across the slide background. */ import * as React from "react"; import { Button, makeStyles } from "@fluentui/react-components"; import { Edit24Regular, DismissRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; /** * Configuration constants for draft watermarks */ const DRAFT_CONFIG = { // Shape name used to identify draft watermarks SHAPE_NAME: "DraftWatermark", // Font configuration FONT: { NAME: "Inter", SIZE: 54, COLOR: "#FFE9E8" // Light pink RGB(255, 233, 232) }, // Text box configuration TEXT_BOX: { LEFT: -330, TOP: 32, WIDTH: 2400, HEIGHT: 540 }, // Watermark text (repeated pattern) TEXT: [ "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT", "DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT" ].join("\n") }; /** * Component-specific styles */ const useStyles = makeStyles({ buttonGrid: { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", width: "100%" }, draftButton: { width: "100%", minWidth: 0, padding: "8px 4px", "& span": { justifyContent: "center" } } }); /** * Interface for master slide processing results */ interface ProcessingResult { processedMasters: number; errorMasters: number; affectedCount: number; // Either added or removed watermarks } /** * DraftButtons component provides UI and functionality to add or remove * draft watermarks on PowerPoint master slides. * * @returns React component */ export const DraftButtons: React.FC = () => { const styles = useStyles(); const commonStyles = useCommonStyles(); const { setStatusMessage, setStatusType, isProcessing, setIsProcessing } = useStatusContext(); /** * Handles errors and updates the status message * * @param error - The error object * @param operation - The operation being performed (for logging) */ const handleError = (error: any, operation: string): void => { setStatusMessage(`Error: ${error.message || "Unknown error occurred"}`); setStatusType("error"); console.error(`${operation} error:`, error); setIsProcessing(false); }; /** * Updates the status message based on processing results * * @param result - The processing result object * @param isAddOperation - Whether this was an add operation (true) or remove operation (false) */ const updateStatusFromResult = (result: ProcessingResult, isAddOperation: boolean): void => { const { processedMasters, errorMasters, affectedCount } = result; if (affectedCount > 0) { const action = isAddOperation ? "Added" : "Removed"; const pluralWatermark = affectedCount > 1 ? 's' : ''; const pluralMasters = processedMasters > 1 ? 's' : ''; setStatusMessage(`${action} draft watermark${pluralWatermark} to ${processedMasters} master slide${pluralMasters}.`); setStatusType("success"); } else if (errorMasters > 0) { const pluralMasters = errorMasters > 1 ? 's' : ''; setStatusMessage(`Failed to ${isAddOperation ? "add" : "remove"} watermarks. Errors on ${errorMasters} master slide${pluralMasters}.`); setStatusType("error"); } else { if (isAddOperation) { setStatusMessage("No master slides found to add draft watermark."); } else { setStatusMessage("No draft watermark found to remove."); } setStatusType(isAddOperation ? "warning" : "info"); } }; /** * Adds draft watermarks to all master slides in the presentation */ const addDraftWatermark = async (): Promise => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { // Get the slide masters collection const masters = context.presentation.slideMasters; masters.load("items"); await context.sync(); if (masters.items.length === 0) { setStatusMessage("Could not access slide masters."); setStatusType("error"); return; } const result = await processAddWatermarks(context, masters.items); updateStatusFromResult(result, true); }); } catch (error) { handleError(error, "Add draft watermark"); } finally { setIsProcessing(false); } }; /** * Processes all master slides to add draft watermarks * * @param context - The PowerPoint API context * @param masters - Array of master slides to process * @returns Processing result with counts of processed and error slides */ const processAddWatermarks = async ( context: PowerPoint.RequestContext, masters: PowerPoint.SlideMaster[] ): Promise => { const result: ProcessingResult = { processedMasters: 0, errorMasters: 0, affectedCount: 0 }; // Process each slide master (usually there's just one) for (let i = 0; i < masters.length; i++) { try { const master = masters[i]; // Create the textbox on the master slide const textBox = master.shapes.addTextBox(""); // Set position and size in a single batch textBox.left = DRAFT_CONFIG.TEXT_BOX.LEFT; textBox.top = DRAFT_CONFIG.TEXT_BOX.TOP; textBox.width = DRAFT_CONFIG.TEXT_BOX.WIDTH; textBox.height = DRAFT_CONFIG.TEXT_BOX.HEIGHT; textBox.name = DRAFT_CONFIG.SHAPE_NAME; // Load textFrame to set text properties textBox.load("textFrame"); await context.sync(); if (textBox.textFrame) { // Load textRange to set text and properties textBox.textFrame.load("textRange"); await context.sync(); // Set text and load font properties in a single batch textBox.textFrame.textRange.text = DRAFT_CONFIG.TEXT; textBox.textFrame.textRange.load("font,paragraphFormat"); await context.sync(); try { // Apply all font properties in a single batch const font = textBox.textFrame.textRange.font; font.name = DRAFT_CONFIG.FONT.NAME; font.size = DRAFT_CONFIG.FONT.SIZE; font.bold = true; font.color = DRAFT_CONFIG.FONT.COLOR; // Set alignment textBox.textFrame.verticalAlignment = "MiddleCentered"; await context.sync(); result.affectedCount++; } catch (fontError) { console.error("Error setting font properties:", fontError); // Continue with default font if custom font fails } } result.processedMasters++; } catch (masterError) { console.error(`Error processing master ${i+1}:`, masterError); result.errorMasters++; // Continue to the next master } } return result; }; /** * Removes draft watermarks from all master slides in the presentation */ const removeDraftWatermark = async (): Promise => { setIsProcessing(true); try { await PowerPoint.run(async (context) => { // Get the slide masters collection const masters = context.presentation.slideMasters; masters.load("items"); await context.sync(); if (masters.items.length === 0) { setStatusMessage("Could not access slide masters."); setStatusType("error"); return; } const result = await processRemoveWatermarks(context, masters.items); updateStatusFromResult(result, false); }); } catch (error) { handleError(error, "Remove draft watermark"); } finally { setIsProcessing(false); } }; /** * Processes all master slides to remove draft watermarks * * @param context - The PowerPoint API context * @param masters - Array of master slides to process * @returns Processing result with counts of processed and affected slides */ const processRemoveWatermarks = async ( context: PowerPoint.RequestContext, masters: PowerPoint.SlideMaster[] ): Promise => { const result: ProcessingResult = { processedMasters: 0, errorMasters: 0, affectedCount: 0 }; // Process each master slide for (let i = 0; i < masters.length; i++) { try { const master = masters[i]; // Load all shapes on the master slide master.shapes.load("items"); await context.sync(); // Collect shapes to delete const shapesToDelete: PowerPoint.Shape[] = []; // Load all shape names in a single batch for (let j = 0; j < master.shapes.items.length; j++) { const shape = master.shapes.items[j]; shape.load("name"); } // Wait for all shape names to load await context.sync(); // Now check names and collect shapes to delete for (let j = 0; j < master.shapes.items.length; j++) { const shape = master.shapes.items[j]; if (shape.name === DRAFT_CONFIG.SHAPE_NAME) { shapesToDelete.push(shape); } } // Delete all matching shapes in one batch shapesToDelete.forEach(shape => shape.delete()); result.affectedCount += shapesToDelete.length; await context.sync(); result.processedMasters++; } catch (masterError) { console.error(`Error processing master slide ${i+1}:`, masterError); result.errorMasters++; // Continue to the next master slide } } return result; }; return (
); }; export default DraftButtons;