Refactor RoundImage component for improved maintainability and consistency with other components
This commit is contained in:
@@ -1,24 +1,126 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
CircleRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import { CircleRegular } from "@fluentui/react-icons";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types";
|
||||
|
||||
// Configuration constants
|
||||
const MASK_COLOR = "red";
|
||||
const SUCCESS_MESSAGE = "Created mask shape. Please select both the image and the oval, then use the 'Shape Format > Merge Shapes > Intersect' command in PowerPoint.";
|
||||
|
||||
export const RoundImage: React.FC = () => {
|
||||
const styles = useCommonStyles();
|
||||
const {
|
||||
statusMessage, setStatusMessage,
|
||||
statusType, setStatusType,
|
||||
isProcessing, setIsProcessing
|
||||
} = useStatusContext();
|
||||
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||
|
||||
const convertToRoundImage = async () => {
|
||||
setIsProcessing(true);
|
||||
/**
|
||||
* Validates that an image shape is selected
|
||||
* @param shapes The collection of selected shapes
|
||||
* @param context The PowerPoint request context
|
||||
* @returns The selected image shape or null if validation fails
|
||||
*/
|
||||
const validateShapeSelection = async (
|
||||
shapes: PowerPoint.ShapeScopedCollection,
|
||||
context: PowerPoint.RequestContext
|
||||
): Promise<PowerPoint.Shape | null> => {
|
||||
// Check if any shape is selected
|
||||
if (shapes.items.length === 0) {
|
||||
setStatusMessage("No shapes are selected. Please select an image.");
|
||||
setStatusType("warning");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the first selected shape
|
||||
const shape = shapes.items[0];
|
||||
|
||||
// Load essential properties
|
||||
shape.load(["type"]);
|
||||
await context.sync();
|
||||
|
||||
// Ensure the shape is a picture using our type-safe utility
|
||||
if (!isPictureShape(shape)) {
|
||||
setStatusMessage("Please select an image.");
|
||||
setStatusType("warning");
|
||||
return null;
|
||||
}
|
||||
|
||||
return shape;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an elliptical mask shape for the image
|
||||
* @param slide The slide to add the mask to
|
||||
* @param imageShape The image shape to mask
|
||||
* @param context The PowerPoint request context
|
||||
* @returns The created mask shape
|
||||
*/
|
||||
const createMaskShape = async (
|
||||
slide: PowerPoint.Slide,
|
||||
imageShape: PowerPoint.Shape,
|
||||
context: PowerPoint.RequestContext
|
||||
): Promise<PowerPoint.Shape> => {
|
||||
// Load current dimensions to maintain aspect ratio
|
||||
imageShape.load(["width", "height", "left", "top", "id"]);
|
||||
await context.sync();
|
||||
|
||||
// Create elliptical mask with proper type
|
||||
const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse);
|
||||
|
||||
maskShape.load(["width", "height", "left", "top", "id"]);
|
||||
await context.sync();
|
||||
|
||||
// Position the mask to match the image
|
||||
maskShape.left = imageShape.left;
|
||||
|
||||
imageShape.lineFormat.load(["weight"]);
|
||||
await context.sync();
|
||||
|
||||
maskShape.top = imageShape.top;
|
||||
maskShape.width = imageShape.width;
|
||||
maskShape.height = imageShape.height;
|
||||
|
||||
// Style the mask
|
||||
maskShape.fill.setSolidColor(MASK_COLOR);
|
||||
maskShape.lineFormat.visible = false;
|
||||
|
||||
return maskShape;
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies the mask to the image and selects both shapes
|
||||
* @param slide The slide containing the shapes
|
||||
* @param imageShape The image shape to mask
|
||||
* @param maskShape The mask shape
|
||||
* @param context The PowerPoint request context
|
||||
*/
|
||||
const applyMaskToImage = async (
|
||||
slide: PowerPoint.Slide,
|
||||
imageShape: PowerPoint.Shape,
|
||||
maskShape: PowerPoint.Shape,
|
||||
context: PowerPoint.RequestContext
|
||||
): Promise<void> => {
|
||||
// Store original dimensions to maintain after selection
|
||||
const width = imageShape.width;
|
||||
const height = imageShape.height;
|
||||
|
||||
// Select both shapes for the user to apply the intersection
|
||||
selectShapesById(slide, [imageShape.id, maskShape.id]);
|
||||
|
||||
// Ensure we maintain the same size
|
||||
imageShape.width = width;
|
||||
imageShape.height = height;
|
||||
|
||||
await context.sync();
|
||||
|
||||
// Update status message with instructions
|
||||
setStatusMessage(SUCCESS_MESSAGE);
|
||||
setStatusType("warning");
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to convert an image to a round image
|
||||
*/
|
||||
const convertToRoundImage = async (): Promise<void> => {
|
||||
try {
|
||||
await PowerPoint.run(async (context) => {
|
||||
// Get the selected shapes
|
||||
@@ -26,89 +128,40 @@ export const RoundImage: React.FC = () => {
|
||||
shapes.load("items");
|
||||
await context.sync();
|
||||
|
||||
if (shapes.items.length === 0) {
|
||||
setStatusMessage("No shapes are selected. Please select an image.");
|
||||
setStatusType("warning");
|
||||
// Validate selection and get the image shape
|
||||
const imageShape = await validateShapeSelection(shapes, context);
|
||||
if (!imageShape) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first selected shape
|
||||
const shape = shapes.items[0];
|
||||
|
||||
// Load essential properties
|
||||
shape.load(["type"]);
|
||||
await context.sync();
|
||||
|
||||
// Ensure the shape is a picture using our type-safe utility
|
||||
if (!isPictureShape(shape)) {
|
||||
setStatusMessage("Please select an image.");
|
||||
setStatusType("warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load current dimensions to maintain aspect ratio
|
||||
shape.load(["width", "height", "left", "top", "id"]);
|
||||
await context.sync();
|
||||
|
||||
// Store current dimensions
|
||||
const width = shape.width;
|
||||
const height = shape.height;
|
||||
|
||||
// Get the current slide using our type-safe utility
|
||||
const slide = getFirstSelectedSlide(context);
|
||||
// Create elliptical mask with proper type
|
||||
const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse);
|
||||
|
||||
maskShape.load(["width", "height", "left", "top", "id"]);
|
||||
await context.sync();
|
||||
|
||||
maskShape.left = shape.left;
|
||||
|
||||
shape.lineFormat.load(["weight"]);
|
||||
await context.sync();
|
||||
|
||||
maskShape.top = shape.top; // + shape.lineFormat.weight;
|
||||
maskShape.width = shape.width;
|
||||
maskShape.height = shape.height;
|
||||
maskShape.fill.setSolidColor("red");
|
||||
maskShape.lineFormat.visible = false;
|
||||
|
||||
setStatusMessage("Created mask shape. Please select both the image and the oval, then use the 'Shape Forma > Merge Shapes > Intersect' command in PowerPoint.");
|
||||
setStatusType("warning");
|
||||
|
||||
// Use our type-safe utility for selecting shapes
|
||||
selectShapesById(slide, [shape.id, maskShape.id]);
|
||||
// Ensure we maintain the same size
|
||||
shape.width = width;
|
||||
shape.height = height;
|
||||
|
||||
await context.sync();
|
||||
|
||||
// Create the mask shape
|
||||
const maskShape = await createMaskShape(slide, imageShape, context);
|
||||
|
||||
// Apply the mask and select both shapes
|
||||
await applyMaskToImage(slide, imageShape, maskShape, context);
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setStatusMessage(`Error: ${errorMessage}`);
|
||||
setStatusType("error");
|
||||
console.error("Round image error:", error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={styles.actionButton}
|
||||
onClick={convertToRoundImage}
|
||||
<ActionButton
|
||||
title="Round Image"
|
||||
icon={<CircleRegular />}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Round Image
|
||||
</Button>
|
||||
onClick={convertToRoundImage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoundImage;
|
||||
export default RoundImage;
|
||||
|
||||
Reference in New Issue
Block a user