Files
powerpoint-toolbox/src/taskpane/components/DraftButtons.tsx
T

364 lines
11 KiB
TypeScript

/**
* @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<void> => {
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<ProcessingResult> => {
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<void> => {
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<ProcessingResult> => {
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 (
<div className={commonStyles.container}>
<div className={styles.buttonGrid}>
<Button
appearance="primary"
className={styles.draftButton}
onClick={addDraftWatermark}
icon={<Edit24Regular />}
disabled={isProcessing}
title="Add Draft"
>
Add
</Button>
<Button
appearance="primary"
className={styles.draftButton}
onClick={removeDraftWatermark}
icon={<DismissRegular />}
disabled={isProcessing}
title="Remove Draft"
>
Remove
</Button>
</div>
</div>
);
};
export default DraftButtons;