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 * as React from "react";
|
||||||
import {
|
import { CircleRegular } from "@fluentui/react-icons";
|
||||||
Button
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import {
|
|
||||||
CircleRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
import { useCommonStyles } from "./commonStyles";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types";
|
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 = () => {
|
export const RoundImage: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
const {
|
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||||
statusMessage, setStatusMessage,
|
|
||||||
statusType, setStatusType,
|
|
||||||
isProcessing, setIsProcessing
|
|
||||||
} = 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 {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
await PowerPoint.run(async (context) => {
|
||||||
// Get the selected shapes
|
// Get the selected shapes
|
||||||
@@ -26,89 +128,40 @@ export const RoundImage: React.FC = () => {
|
|||||||
shapes.load("items");
|
shapes.load("items");
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
if (shapes.items.length === 0) {
|
// Validate selection and get the image shape
|
||||||
setStatusMessage("No shapes are selected. Please select an image.");
|
const imageShape = await validateShapeSelection(shapes, context);
|
||||||
setStatusType("warning");
|
if (!imageShape) {
|
||||||
return;
|
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
|
// Get the current slide using our type-safe utility
|
||||||
const slide = getFirstSelectedSlide(context);
|
const slide = getFirstSelectedSlide(context);
|
||||||
// Create elliptical mask with proper type
|
|
||||||
const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse);
|
// Create the mask shape
|
||||||
|
const maskShape = await createMaskShape(slide, imageShape, context);
|
||||||
maskShape.load(["width", "height", "left", "top", "id"]);
|
|
||||||
await context.sync();
|
// Apply the mask and select both shapes
|
||||||
|
await applyMaskToImage(slide, imageShape, maskShape, context);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
setStatusMessage(`Error: ${errorMessage}`);
|
setStatusMessage(`Error: ${errorMessage}`);
|
||||||
setStatusType("error");
|
setStatusType("error");
|
||||||
console.error("Round image error:", error);
|
console.error("Round image error:", error);
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button
|
<ActionButton
|
||||||
appearance="primary"
|
title="Round Image"
|
||||||
className={styles.actionButton}
|
|
||||||
onClick={convertToRoundImage}
|
|
||||||
icon={<CircleRegular />}
|
icon={<CircleRegular />}
|
||||||
disabled={isProcessing}
|
onClick={convertToRoundImage}
|
||||||
>
|
/>
|
||||||
Round Image
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoundImage;
|
export default RoundImage;
|
||||||
|
|||||||
Reference in New Issue
Block a user