mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-03 11:34:39 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dfbca53b4 |
No files matched your search
+1
-1
@@ -89,7 +89,7 @@
|
||||
"color-name": "2.1.1",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.51.0",
|
||||
"cropperjs": "1.7.0",
|
||||
"cropperjs": "2.3.1",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.4.0",
|
||||
"decimal.js": "10.6.0",
|
||||
|
||||
Generated
+107
-5
@@ -328,8 +328,8 @@ importers:
|
||||
specifier: 3.51.0
|
||||
version: 3.51.0
|
||||
cropperjs:
|
||||
specifier: 1.7.0
|
||||
version: 1.7.0
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1
|
||||
culori:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2
|
||||
@@ -1799,6 +1799,39 @@ packages:
|
||||
peerDependencies:
|
||||
commander: ~15.0.0
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-UH3VMyjn58PdwYkPiTHuvRUjGzmyKr6E2pYWxTxN/04wnjgYLA77B5u5c1HhV6vD1ZKSvxJ/i9TkiySR9P1D6A==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-Vz2heRwr2nd2rZ161vKyE4JwYV3eSodGCGGZq39TR0LICGOOeggLq0R664Af/eFB9PU8+tfJ42lWLoUdxQlVJQ==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-6LDaUDxH2xdA7biTcqnihPQVHjlT4bXysA403s9rU4jo5f4pPnFHtNbB+FQTYKNz5EKun52WgVtR+6oWbmbxlg==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-ygLw89PWu9k8SuKW7rjRBpD6ZfmtVr5ayWux9YKHU7PxX0SeL5kbCD+PpOxQB1ycW4UcyH0tHt77EWnsdSoOHw==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-rPE7OX/BPTw66+DjpN5aOuPb1xSAjQMgAJZXdRgmeJnn0YcDDhfGQm8VwcL8/PMLxtWFcKEM+aUiezbO01j4fg==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-b8sabaFbHeQYY459uItDIeDY0IisZBO/wF+SYhKvAgI8TExj/f2mNcBZXjPHYCkWel2+1BSUTTNZxy+tR8sDEA==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-X5ceZsy7iBxuIRhsL05H9+cPYzxZuvM7/9j5irXIbNsDiNwTedxIHdjhEhPbUYa+FaNn9wsKId24AEyH43JTGw==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-EdrYDbnkj0IOl/kyWKzzvcDRobr7Tfzho7FkVu8mpSk1ILDHmVf8pfZrYVwou8l4znLqp8ZqwuabAezJPfC2Yg==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-CgrLOx0qERWlfJK1Ed6ePq7SXcB6gLHtj3mtDVypyzti70fJdmNxrFm5Ucbq8vpp2Yo3tW2L6uDI0IBDoiT17A==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-+D5rFlfFlWSL1lp0vPYI59/R6XM2vMnTSX0/B1zKRvmkc0t0N0m9lxVsaDb+9RkHx6PwAkBBNPZSRUeau33jMg==}
|
||||
|
||||
'@cropper/[email protected]':
|
||||
resolution: {integrity: sha512-b5qFTwws/osx0asIGLK1V//hdL08frGvfpi6s+p6zj5XH05ofVcz0Xl+Tot7sUx12pbfhhAlsquQTOJ1IsOoFw==}
|
||||
|
||||
'@csstools/[email protected]':
|
||||
resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
@@ -4381,8 +4414,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
|
||||
|
||||
cropperjs@1.7.0:
|
||||
resolution: {integrity: sha512-AkGnZxr7xAJBa63ObXq5ZKLkh9c8wisC+Ym8VzIDn3KZ+6qRTbBnOl2w2dFOTMzLP5IcfOpEK9PICDywNXhncg==}
|
||||
cropperjs@2.3.1:
|
||||
resolution: {integrity: sha512-bgy+iHRFjsBxBCriXkTuFuHRJGSGZISuqDGhDQG7gLK0F7qOzhH06bdXzGf4b0bZSQ+mApakybiW9xvKnS9dbg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
@@ -9264,6 +9297,72 @@ snapshots:
|
||||
dependencies:
|
||||
commander: 15.0.0
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/element-canvas': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/element-canvas': 2.3.1
|
||||
'@cropper/element-image': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/element-canvas': 2.3.1
|
||||
'@cropper/element-selection': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/element-canvas': 2.3.1
|
||||
'@cropper/element-image': 2.3.1
|
||||
'@cropper/element-selection': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
'@cropper/[email protected]':
|
||||
dependencies:
|
||||
'@cropper/element': 2.3.1
|
||||
'@cropper/element-canvas': 2.3.1
|
||||
'@cropper/element-crosshair': 2.3.1
|
||||
'@cropper/element-grid': 2.3.1
|
||||
'@cropper/element-handle': 2.3.1
|
||||
'@cropper/element-image': 2.3.1
|
||||
'@cropper/element-selection': 2.3.1
|
||||
'@cropper/element-shade': 2.3.1
|
||||
'@cropper/element-viewer': 2.3.1
|
||||
|
||||
'@cropper/[email protected]': {}
|
||||
|
||||
'@csstools/[email protected]': {}
|
||||
|
||||
'@csstools/[email protected](@csstools/[email protected](@csstools/[email protected]))(@csstools/[email protected])':
|
||||
@@ -11996,7 +12095,10 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
cropperjs@1.7.0: {}
|
||||
cropperjs@2.3.1:
|
||||
dependencies:
|
||||
'@cropper/elements': 2.3.1
|
||||
'@cropper/utils': 2.3.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import Cropper from "cropperjs";
|
||||
// @ts-ignore
|
||||
import cropperCss from "cropperjs/dist/cropper.css";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing, unsafeCSS } from "lit";
|
||||
import "cropperjs";
|
||||
import type {
|
||||
CropperCanvas,
|
||||
CropperGrid,
|
||||
CropperHandle,
|
||||
CropperImage,
|
||||
CropperSelection,
|
||||
CropperShade,
|
||||
} from "cropperjs";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
@@ -16,22 +24,74 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import { DialogMixin } from "../dialog-mixin";
|
||||
import type { HaImageCropperDialogParams } from "./show-image-cropper-dialog";
|
||||
|
||||
interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const MIN_SELECTION_SIZE = 50;
|
||||
|
||||
const INITIAL_COVERAGE = 0.8;
|
||||
|
||||
const EPSILON = 0.05;
|
||||
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
|
||||
const contains = (outer: Rect, inner: Rect): boolean =>
|
||||
inner.x >= outer.x - EPSILON &&
|
||||
inner.y >= outer.y - EPSILON &&
|
||||
inner.x + inner.width <= outer.x + outer.width + EPSILON &&
|
||||
inner.y + inner.height <= outer.y + outer.height + EPSILON;
|
||||
|
||||
const intersect = (a: Rect, b: Rect): Rect => {
|
||||
const x = Math.max(a.x, b.x);
|
||||
const y = Math.max(a.y, b.y);
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width: Math.min(a.x + a.width, b.x + b.width) - x,
|
||||
height: Math.min(a.y + a.height, b.y + b.height) - y,
|
||||
};
|
||||
};
|
||||
|
||||
const scaleRect = ({ x, y, width, height }: Rect, ratio: number): Rect => ({
|
||||
x: x * ratio,
|
||||
y: y * ratio,
|
||||
width: width * ratio,
|
||||
height: height * ratio,
|
||||
});
|
||||
|
||||
@customElement("image-cropper-dialog")
|
||||
export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>()(
|
||||
export class HaImagecropperDialog extends DirtyStateProviderMixin<Rect>()(
|
||||
DialogMixin<HaImageCropperDialogParams>(LitElement)
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@query("img") private _image!: HTMLImageElement;
|
||||
@query("cropper-image") private _image?: CropperImage;
|
||||
|
||||
private _cropper?: Cropper;
|
||||
@query("cropper-selection") private _selection?: CropperSelection;
|
||||
|
||||
@state() private _imageSize?: { width: number; height: number };
|
||||
|
||||
@state() private _shown = false;
|
||||
|
||||
@state() private _cropReady = false;
|
||||
|
||||
@state() private _isTargetAspectRatio?: boolean;
|
||||
|
||||
private _imageUrl?: string;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _canvasWidth?: number;
|
||||
|
||||
private _refitting = false;
|
||||
|
||||
private _cropped = false;
|
||||
|
||||
private _closing = false;
|
||||
@@ -41,51 +101,176 @@ export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>(
|
||||
if (!this._cropped) {
|
||||
this.params?.cancelCallback?.();
|
||||
}
|
||||
this._cropper?.destroy();
|
||||
this._cropper = undefined;
|
||||
if (this._imageUrl) {
|
||||
URL.revokeObjectURL(this._imageUrl);
|
||||
this._imageUrl = undefined;
|
||||
}
|
||||
this._resizeObserver?.disconnect();
|
||||
this._resizeObserver = undefined;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
this._image.src = URL.createObjectURL(this.params.file);
|
||||
this._cropper = new Cropper(this._image, {
|
||||
aspectRatio: this.params.options.aspectRatio,
|
||||
viewMode: 1,
|
||||
dragMode: "move",
|
||||
minCropBoxWidth: 50,
|
||||
ready: () => {
|
||||
this._isTargetAspectRatio = this._checkMatchAspectRatio();
|
||||
URL.revokeObjectURL(this._image!.src);
|
||||
this._initDirtyTracking({ type: "deep" }, this._cropper!.getData());
|
||||
this._cropReady = true;
|
||||
},
|
||||
crop: () => {
|
||||
if (this._cropReady) {
|
||||
this._updateDirtyState(this._cropper!.getData());
|
||||
}
|
||||
},
|
||||
});
|
||||
this._imageUrl = URL.createObjectURL(this.params.file);
|
||||
this._loadImageSize(this._imageUrl);
|
||||
}
|
||||
|
||||
private _checkMatchAspectRatio(): boolean {
|
||||
private async _loadImageSize(url: string) {
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
try {
|
||||
await image.decode();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
this._imageSize = {
|
||||
width: image.naturalWidth,
|
||||
height: image.naturalHeight,
|
||||
};
|
||||
this._isTargetAspectRatio = this._checkMatchAspectRatio(
|
||||
image.naturalWidth,
|
||||
image.naturalHeight
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
!this._cropReady &&
|
||||
(changedProps.has("_imageSize") || changedProps.has("_shown")) &&
|
||||
this._image
|
||||
) {
|
||||
this._initCropper(this._image);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initCropper(image: CropperImage) {
|
||||
try {
|
||||
await image.$ready();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!this.isConnected || image !== this._image) {
|
||||
return;
|
||||
}
|
||||
const selection = this._selection!;
|
||||
if (!selection.width) {
|
||||
await new Promise<void>((resolve) => {
|
||||
selection.addEventListener("change", () => resolve(), { once: true });
|
||||
});
|
||||
if (!this.isConnected || image !== this._image) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
this._cropData(this._selectionRect(), this._imageRect())
|
||||
);
|
||||
this._cropReady = true;
|
||||
this._resizeObserver = new ResizeObserver((entries) => {
|
||||
this._refit(entries[0].contentRect);
|
||||
});
|
||||
this._resizeObserver.observe(image.parentElement!);
|
||||
}
|
||||
|
||||
private _selectionRect(): Rect {
|
||||
const { x, y, width, height } = this._selection!;
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
private _canvasRect(): Rect {
|
||||
const { width, height } =
|
||||
this._image!.parentElement!.getBoundingClientRect();
|
||||
return { x: 0, y: 0, width, height };
|
||||
}
|
||||
|
||||
private _imageRect(): Rect {
|
||||
const canvasRect = this._image!.parentElement!.getBoundingClientRect();
|
||||
const imageRect = this._image!.getBoundingClientRect();
|
||||
return {
|
||||
x: imageRect.x - canvasRect.x,
|
||||
y: imageRect.y - canvasRect.y,
|
||||
width: imageRect.width,
|
||||
height: imageRect.height,
|
||||
};
|
||||
}
|
||||
|
||||
private _cropData(selection: Rect, image: Rect): Rect {
|
||||
const scale = image.width / this._imageSize!.width;
|
||||
return {
|
||||
x: Math.round((selection.x - image.x) / scale),
|
||||
y: Math.round((selection.y - image.y) / scale),
|
||||
width: Math.round(selection.width / scale),
|
||||
height: Math.round(selection.height / scale),
|
||||
};
|
||||
}
|
||||
|
||||
private _refit(canvas: { width: number; height: number }) {
|
||||
const previous = this._canvasWidth;
|
||||
this._canvasWidth = canvas.width;
|
||||
if (!previous || !canvas.width || previous === canvas.width) {
|
||||
return;
|
||||
}
|
||||
const current = this._selectionRect();
|
||||
const ratio = Math.min(
|
||||
canvas.width / previous,
|
||||
canvas.height / current.height
|
||||
);
|
||||
const image = scaleRect(this._imageRect(), ratio);
|
||||
const selection = scaleRect(current, ratio);
|
||||
const dy =
|
||||
image.height <= canvas.height
|
||||
? (canvas.height - image.height) / 2 - image.y
|
||||
: clamp(
|
||||
0,
|
||||
-selection.y,
|
||||
canvas.height - selection.y - selection.height
|
||||
);
|
||||
image.y += dy;
|
||||
selection.y += dy;
|
||||
const { width, height } = this._imageSize!;
|
||||
this._refitting = true;
|
||||
this._image!.$setTransform(
|
||||
image.width / width,
|
||||
0,
|
||||
0,
|
||||
image.height / height,
|
||||
image.x + image.width / 2 - width / 2,
|
||||
image.y + image.height / 2 - height / 2
|
||||
);
|
||||
this._selection!.$change(
|
||||
selection.x,
|
||||
selection.y,
|
||||
selection.width,
|
||||
selection.height
|
||||
);
|
||||
this._refitting = false;
|
||||
}
|
||||
|
||||
private _checkMatchAspectRatio(
|
||||
naturalWidth: number,
|
||||
naturalHeight: number
|
||||
): boolean {
|
||||
const targetRatio = this.params?.options.aspectRatio;
|
||||
if (!targetRatio) {
|
||||
return true;
|
||||
}
|
||||
const imageData = this._cropper!.getImageData();
|
||||
if (imageData.aspectRatio === targetRatio) {
|
||||
if (naturalWidth / naturalHeight === targetRatio) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the image is not exactly the aspect ratio see if it is within a pixel.
|
||||
if (imageData.naturalWidth > imageData.naturalHeight) {
|
||||
const targetHeight = imageData.naturalWidth / targetRatio;
|
||||
return Math.abs(targetHeight - imageData.naturalHeight) <= 1;
|
||||
if (naturalWidth > naturalHeight) {
|
||||
const targetHeight = naturalWidth / targetRatio;
|
||||
return Math.abs(targetHeight - naturalHeight) <= 1;
|
||||
}
|
||||
const targetWidth = imageData.naturalHeight * targetRatio;
|
||||
return Math.abs(targetWidth - imageData.naturalWidth) <= 1;
|
||||
const targetWidth = naturalHeight * targetRatio;
|
||||
return Math.abs(targetWidth - naturalWidth) <= 1;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
@@ -93,23 +278,65 @@ export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>(
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const label = this._i18n.localize("ui.dialogs.image_cropper.crop_image");
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.dialogs.image_cropper.crop_image"
|
||||
)}
|
||||
header-title=${label}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@wa-hide=${this._handleHide}
|
||||
@after-show=${this._handleAfterShow}
|
||||
>
|
||||
<div
|
||||
class="container ${classMap({
|
||||
round: Boolean(this.params.options.round),
|
||||
})}"
|
||||
>
|
||||
<img
|
||||
alt=${this._i18n.localize("ui.dialogs.image_cropper.crop_image")}
|
||||
/>
|
||||
${
|
||||
this._imageSize && this._shown
|
||||
? html`
|
||||
<cropper-canvas
|
||||
background
|
||||
style=${styleMap({
|
||||
aspectRatio: `${this._imageSize.width} / ${this._imageSize.height}`,
|
||||
})}
|
||||
>
|
||||
<cropper-image
|
||||
src=${this._imageUrl!}
|
||||
alt=${label}
|
||||
scalable
|
||||
translatable
|
||||
@change=${this._handleImageChange}
|
||||
></cropper-image>
|
||||
<cropper-shade hidden></cropper-shade>
|
||||
<cropper-handle action="move" plain></cropper-handle>
|
||||
<cropper-selection
|
||||
aspect-ratio=${ifDefined(
|
||||
this.params.options.aspectRatio || undefined
|
||||
)}
|
||||
initial-coverage=${INITIAL_COVERAGE}
|
||||
precise
|
||||
movable
|
||||
resizable
|
||||
outlined
|
||||
@change=${this._handleSelectionChange}
|
||||
>
|
||||
<cropper-grid role="grid" bordered covered></cropper-grid>
|
||||
<cropper-handle action="move" plain></cropper-handle>
|
||||
<cropper-handle action="n-resize"></cropper-handle>
|
||||
<cropper-handle action="e-resize"></cropper-handle>
|
||||
<cropper-handle action="s-resize"></cropper-handle>
|
||||
<cropper-handle action="w-resize"></cropper-handle>
|
||||
<cropper-handle action="ne-resize"></cropper-handle>
|
||||
<cropper-handle action="nw-resize"></cropper-handle>
|
||||
<cropper-handle action="se-resize"></cropper-handle>
|
||||
<cropper-handle action="sw-resize"></cropper-handle>
|
||||
</cropper-selection>
|
||||
</cropper-canvas>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@@ -127,9 +354,7 @@ export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>(
|
||||
appearance="plain"
|
||||
@click=${this._useOriginal}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.image_cropper.use_original"
|
||||
)}
|
||||
${this._i18n.localize("ui.dialogs.image_cropper.use_original")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing
|
||||
@@ -146,17 +371,68 @@ export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>(
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAfterShow() {
|
||||
this._shown = true;
|
||||
}
|
||||
|
||||
private _handleHide(ev: Event) {
|
||||
// Ignore hide events from nested overlays and blocked dismissals.
|
||||
if (ev.eventPhase !== Event.AT_TARGET || ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
this._closing = true;
|
||||
}
|
||||
|
||||
private _cropImage() {
|
||||
const canvas = this._cropper?.getCroppedCanvas();
|
||||
if (!canvas) {
|
||||
private _handleSelectionChange(ev: CustomEvent<Rect>) {
|
||||
if (!this._cropReady || this._refitting) {
|
||||
return;
|
||||
}
|
||||
const selection = this._selection!;
|
||||
const image = this._imageRect();
|
||||
const next = ev.detail;
|
||||
if (
|
||||
(next.width < MIN_SELECTION_SIZE && next.width < selection.width) ||
|
||||
!contains(intersect(image, this._canvasRect()), next)
|
||||
) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
this._updateDirtyState(this._cropData(next, image));
|
||||
}
|
||||
|
||||
private _handleImageChange(ev: CustomEvent<Rect>) {
|
||||
if (!this._cropReady || this._refitting) {
|
||||
return;
|
||||
}
|
||||
const selection = this._selectionRect();
|
||||
if (!contains(ev.detail, selection)) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
this._updateDirtyState(this._cropData(selection, ev.detail));
|
||||
}
|
||||
|
||||
private async _cropImage() {
|
||||
const selection = this._selection;
|
||||
if (!selection || !this._cropReady) {
|
||||
return;
|
||||
}
|
||||
const image = this._imageRect();
|
||||
const rect = this._selectionRect();
|
||||
const x = clamp(rect.x, image.x, image.x + image.width - rect.width);
|
||||
const y = clamp(rect.y, image.y, image.y + image.height - rect.height);
|
||||
if (x !== rect.x || y !== rect.y) {
|
||||
selection.$change(x, y);
|
||||
}
|
||||
const crop = this._cropData(this._selectionRect(), image);
|
||||
let canvas: HTMLCanvasElement;
|
||||
try {
|
||||
canvas = await selection.$toCanvas({
|
||||
width: crop.width,
|
||||
beforeDraw: (_context, target) => {
|
||||
target.height = crop.height;
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const params = this.params!;
|
||||
@@ -188,22 +464,22 @@ export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>(
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
${unsafeCSS(cropperCss)}
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
cropper-canvas {
|
||||
width: 100%;
|
||||
direction: ltr;
|
||||
--theme-color: var(--primary-color);
|
||||
}
|
||||
.container.round .cropper-view-box,
|
||||
.container.round .cropper-face {
|
||||
cropper-handle {
|
||||
--theme-color: var(--primary-color);
|
||||
}
|
||||
.container.round cropper-selection,
|
||||
.container.round cropper-shade,
|
||||
.container.round cropper-grid {
|
||||
border-radius: var(--ha-picture-border-radius);
|
||||
}
|
||||
.cropper-line,
|
||||
.cropper-point,
|
||||
.cropper-point.point-se::before {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -212,5 +488,11 @@ export class HaImagecropperDialog extends DirtyStateProviderMixin<Cropper.Data>(
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"image-cropper-dialog": HaImagecropperDialog;
|
||||
"cropper-canvas": CropperCanvas;
|
||||
"cropper-image": CropperImage;
|
||||
"cropper-shade": CropperShade;
|
||||
"cropper-handle": CropperHandle;
|
||||
"cropper-selection": CropperSelection;
|
||||
"cropper-grid": CropperGrid;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user