@streamscloud/kit 0.85.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/core/files/file-decorators.d.ts +3 -0
- package/dist/core/files/file-decorators.js +2 -0
- package/dist/core/files/file-validation-rules.d.ts +3 -8
- package/dist/core/files/file-with-blob-data-helper.d.ts +1 -1
- package/dist/core/files/file-with-blob-data-helper.js +1 -1
- package/dist/core/files/image-resizer.d.ts +17 -29
- package/dist/core/files/image-resizer.js +30 -57
- package/dist/core/files/index.d.ts +4 -4
- package/dist/core/files/index.js +2 -2
- package/dist/core/files/types.d.ts +14 -2
- package/dist/core/files/types.js +1 -10
- package/dist/core/files/upload-media-store.svelte.d.ts +11 -17
- package/dist/core/files/upload-media-store.svelte.js +32 -35
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +8 -12
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.d.ts +6 -13
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.js +6 -18
- package/dist/ui/cropper/image-editor-dialog/open-image-editor-dialog.d.ts +6 -1
- package/dist/ui/cropper/image-editor-dialog/open-image-editor-dialog.js +6 -1
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +8 -5
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +6 -2
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +18 -11
- package/dist/ui/image/cmp.image.svelte +1 -0
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@ without it agent sessions run with no hooks and no shared skills. Re-run after c
|
|
|
15
15
|
## Checks
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm run check # svelte-check + tsc for node files
|
|
19
|
-
npm run lint
|
|
18
|
+
npm run check # the gate: strict svelte-check + tsc for node files + eslint + prettier --check
|
|
19
|
+
npm run lint:fix # eslint --fix + prettier --write
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Release
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AspectRatioBound, FileValidationRule, VideoOrientationValue } from './file-validation-types';
|
|
2
|
+
import type { ResizeLimits } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
|
|
4
5
|
* rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
|
|
@@ -21,10 +22,7 @@ export declare const FileRules: {
|
|
|
21
22
|
AudioDuration: (maxSeconds: number, message?: string) => FileValidationRule;
|
|
22
23
|
AudioOnly: (message?: string) => FileValidationRule;
|
|
23
24
|
Extensions: (extensions: string[], message?: string) => FileValidationRule;
|
|
24
|
-
ImageDimensions: (limits:
|
|
25
|
-
max1: number;
|
|
26
|
-
max2: number;
|
|
27
|
-
}, message?: string) => FileValidationRule;
|
|
25
|
+
ImageDimensions: (limits: ResizeLimits, message?: string) => FileValidationRule;
|
|
28
26
|
ImageMinDimensions: (limits: {
|
|
29
27
|
min1: number;
|
|
30
28
|
min2: number;
|
|
@@ -55,9 +53,6 @@ export declare const FileRules: {
|
|
|
55
53
|
VideoOrientation: (orientation: VideoOrientationValue, message?: string, options?: {
|
|
56
54
|
tolerancePercent?: number;
|
|
57
55
|
}) => FileValidationRule;
|
|
58
|
-
VideoDimensions: (limits:
|
|
59
|
-
max1: number;
|
|
60
|
-
max2: number;
|
|
61
|
-
}, message?: string) => FileValidationRule;
|
|
56
|
+
VideoDimensions: (limits: ResizeLimits, message?: string) => FileValidationRule;
|
|
62
57
|
VideoDuration: (maxSeconds: number, message?: string) => FileValidationRule;
|
|
63
58
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Base64Helper } from './base64-helper';
|
|
2
|
+
import { toBlobWithName } from './file-decorators';
|
|
2
3
|
import { FileHelper } from './file-helper';
|
|
3
4
|
import { fetchFile } from './file-service';
|
|
4
|
-
import { toBlobWithName } from './types';
|
|
5
5
|
import { default as mime } from 'mime';
|
|
6
6
|
import { nanoid } from 'nanoid';
|
|
7
7
|
export class FileWithBlobDataHelper {
|
|
@@ -1,31 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { ResizedBlob, ResizeLimits, SizedFile } from './types';
|
|
2
|
+
export declare const DEFAULT_RESIZE_LIMITS: Readonly<ResizeLimits>;
|
|
3
|
+
type PixelSize = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
/** `size` rounded down to whole pixels, at least one per side. */
|
|
8
|
+
export declare const toPixelSize: (size: PixelSize) => PixelSize;
|
|
9
|
+
/** `size` downscaled, keeping its aspect ratio, until it fits `limits` (never enlarged), in whole pixels. */
|
|
10
|
+
export declare const fitSize: (size: PixelSize, limits: ResizeLimits) => PixelSize;
|
|
11
|
+
/** `resizeBlob` for a file: always a new `File` under the source's name, with its size. */
|
|
12
|
+
export declare const resizeImage: (file: File, limits: ResizeLimits) => Promise<SizedFile>;
|
|
6
13
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* and which one applies to the short side of the image.
|
|
11
|
-
*
|
|
12
|
-
* - If the image already fits within the constraints (both sides are within their respective limits),
|
|
13
|
-
* it is returned unchanged.
|
|
14
|
-
* - If both constraints are provided, the image is resized so that the long side matches its limit.
|
|
15
|
-
* However, if this causes the short side to exceed its limit, the short side constraint is applied instead.
|
|
16
|
-
* - If only one constraint is provided, it is applied to the longer side, and the other side is scaled proportionally.
|
|
17
|
-
* - If both constraints are zero or negative, the original image is returned without modifications.
|
|
18
|
-
*
|
|
19
|
-
* @param blob - The original image as a `Blob` object.
|
|
20
|
-
* @param limits - An object containing two numeric constraints, `max1` and `max2`.
|
|
21
|
-
* - **`max1`** - One of the two size constraints.
|
|
22
|
-
* - **`max2`** - The second size constraint.
|
|
23
|
-
* **Note:** `max1` and `max2` are not tied to specific dimensions (width or height).
|
|
24
|
-
* The function determines which one applies to the long side and which to the short side.
|
|
25
|
-
*
|
|
26
|
-
* @returns A promise that resolves to a new resized `Blob` object, or the original `Blob` if resizing is not needed.
|
|
14
|
+
* Downscales an image, keeping its aspect ratio, until both sides fit `limits` (see `ResizeLimits`).
|
|
15
|
+
* Only JPEG, PNG and WebP are re-encoded; an image that already fits, or of any other type (SVG, GIF, …),
|
|
16
|
+
* comes back as the very same `Blob`. Either way the result carries the image's pixel size.
|
|
27
17
|
*/
|
|
28
|
-
export declare const resizeBlob:
|
|
29
|
-
|
|
30
|
-
max2: number;
|
|
31
|
-
}) => Promise<Blob>;
|
|
18
|
+
export declare const resizeBlob: (blob: Blob, limits: ResizeLimits) => Promise<ResizedBlob>;
|
|
19
|
+
export {};
|
|
@@ -1,70 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
export const DEFAULT_RESIZE_LIMITS = Object.freeze({ max1: 1600, max2: 1000 });
|
|
2
|
+
const RESIZABLE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
|
3
|
+
// A scaled side lands a hair under a whole number (999.9999999999999); a bare floor would drop a pixel.
|
|
4
|
+
const PIXEL_EPSILON = 1e-6;
|
|
5
|
+
/** `size` rounded down to whole pixels, at least one per side. */
|
|
6
|
+
export const toPixelSize = (size) => ({
|
|
7
|
+
width: Math.max(1, Math.floor(size.width + PIXEL_EPSILON)),
|
|
8
|
+
height: Math.max(1, Math.floor(size.height + PIXEL_EPSILON))
|
|
9
|
+
});
|
|
10
|
+
/** `size` downscaled, keeping its aspect ratio, until it fits `limits` (never enlarged), in whole pixels. */
|
|
11
|
+
export const fitSize = (size, limits) => {
|
|
12
|
+
const [longMax, shortMax] = limits.max1 >= limits.max2 ? [limits.max1, limits.max2] : [limits.max2, limits.max1];
|
|
13
|
+
const scale = Math.min(1, longMax / Math.max(size.width, size.height), shortMax / Math.min(size.width, size.height));
|
|
14
|
+
return toPixelSize({ width: size.width * scale, height: size.height * scale });
|
|
15
|
+
};
|
|
16
|
+
/** `resizeBlob` for a file: always a new `File` under the source's name, with its size. */
|
|
3
17
|
export const resizeImage = async (file, limits) => {
|
|
4
|
-
const blob = await resizeBlob(file, limits);
|
|
5
|
-
|
|
6
|
-
if (!(blob instanceof File)) {
|
|
7
|
-
result.name = file.name;
|
|
8
|
-
}
|
|
9
|
-
return result;
|
|
18
|
+
const { blob, width, height } = await resizeBlob(file, limits);
|
|
19
|
+
return Object.assign(new File([blob], file.name, { type: blob.type }), { width, height });
|
|
10
20
|
};
|
|
11
21
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* and which one applies to the short side of the image.
|
|
16
|
-
*
|
|
17
|
-
* - If the image already fits within the constraints (both sides are within their respective limits),
|
|
18
|
-
* it is returned unchanged.
|
|
19
|
-
* - If both constraints are provided, the image is resized so that the long side matches its limit.
|
|
20
|
-
* However, if this causes the short side to exceed its limit, the short side constraint is applied instead.
|
|
21
|
-
* - If only one constraint is provided, it is applied to the longer side, and the other side is scaled proportionally.
|
|
22
|
-
* - If both constraints are zero or negative, the original image is returned without modifications.
|
|
23
|
-
*
|
|
24
|
-
* @param blob - The original image as a `Blob` object.
|
|
25
|
-
* @param limits - An object containing two numeric constraints, `max1` and `max2`.
|
|
26
|
-
* - **`max1`** - One of the two size constraints.
|
|
27
|
-
* - **`max2`** - The second size constraint.
|
|
28
|
-
* **Note:** `max1` and `max2` are not tied to specific dimensions (width or height).
|
|
29
|
-
* The function determines which one applies to the long side and which to the short side.
|
|
30
|
-
*
|
|
31
|
-
* @returns A promise that resolves to a new resized `Blob` object, or the original `Blob` if resizing is not needed.
|
|
22
|
+
* Downscales an image, keeping its aspect ratio, until both sides fit `limits` (see `ResizeLimits`).
|
|
23
|
+
* Only JPEG, PNG and WebP are re-encoded; an image that already fits, or of any other type (SVG, GIF, …),
|
|
24
|
+
* comes back as the very same `Blob`. Either way the result carries the image's pixel size.
|
|
32
25
|
*/
|
|
33
26
|
export const resizeBlob = (blob, limits) => {
|
|
34
|
-
const { max1, max2 } = limits;
|
|
35
|
-
if (max1 <= 0 && max2 <= 0) {
|
|
36
|
-
return Promise.resolve(blob);
|
|
37
|
-
}
|
|
38
27
|
return new Promise((resolve, reject) => {
|
|
39
28
|
const image = new Image();
|
|
40
29
|
image.src = URL.createObjectURL(blob);
|
|
41
30
|
image.onload = () => {
|
|
42
31
|
const { width, height } = image;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const originalShortSide = isHorizontal ? height : width;
|
|
46
|
-
const [longSideMax, shortSideMax] = max1 >= max2 ? [max1, max2] : [max2, max1];
|
|
47
|
-
if (longSideMax > 0 && originalLongSide <= longSideMax && shortSideMax > 0 && originalShortSide <= shortSideMax) {
|
|
48
|
-
URL.revokeObjectURL(image.src);
|
|
49
|
-
return resolve(blob);
|
|
50
|
-
}
|
|
51
|
-
let newLongSide = Math.min(originalLongSide, longSideMax);
|
|
52
|
-
let newShortSide = originalShortSide * (newLongSide / originalLongSide);
|
|
53
|
-
if (shortSideMax > 0 && newShortSide > shortSideMax) {
|
|
54
|
-
const reducer = newShortSide / shortSideMax;
|
|
55
|
-
newShortSide = shortSideMax;
|
|
56
|
-
newLongSide = newLongSide / reducer;
|
|
57
|
-
}
|
|
58
|
-
newLongSide = Math.floor(newLongSide);
|
|
59
|
-
newShortSide = Math.floor(newShortSide);
|
|
60
|
-
// Skip resize if dimensions unchanged (avoids lossy re-encoding)
|
|
61
|
-
if (newLongSide === originalLongSide && newShortSide === originalShortSide) {
|
|
32
|
+
const { width: newWidth, height: newHeight } = fitSize({ width, height }, limits);
|
|
33
|
+
if (!RESIZABLE_TYPES.has(blob.type) || (newWidth === width && newHeight === height)) {
|
|
62
34
|
URL.revokeObjectURL(image.src);
|
|
63
|
-
return resolve(blob);
|
|
35
|
+
return resolve({ blob, width, height });
|
|
64
36
|
}
|
|
65
|
-
const
|
|
66
|
-
const newHeight = isHorizontal ? newShortSide : newLongSide;
|
|
67
|
-
const canvas = getCanvas();
|
|
37
|
+
const canvas = document.createElement('canvas');
|
|
68
38
|
canvas.width = newWidth;
|
|
69
39
|
canvas.height = newHeight;
|
|
70
40
|
const ctx = canvas.getContext('2d');
|
|
@@ -75,9 +45,12 @@ export const resizeBlob = (blob, limits) => {
|
|
|
75
45
|
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
|
76
46
|
canvas.toBlob((resizedBlob) => {
|
|
77
47
|
URL.revokeObjectURL(image.src);
|
|
78
|
-
|
|
48
|
+
// Safari caps total canvas memory and holds a dropped canvas until GC; shrinking it to 1×1 releases it now.
|
|
49
|
+
canvas.width = 1;
|
|
50
|
+
canvas.height = 1;
|
|
51
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
79
52
|
if (resizedBlob) {
|
|
80
|
-
resolve(resizedBlob);
|
|
53
|
+
resolve({ blob: resizedBlob, width: newWidth, height: newHeight });
|
|
81
54
|
}
|
|
82
55
|
else {
|
|
83
56
|
reject(new Error('Failed to create blob from canvas'));
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl } from './types';
|
|
1
|
+
export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl, ResizedBlob, ResizeLimits, SizedFile } from './types';
|
|
2
2
|
export type { AspectRatioBound, FileValidationResult, FileValidationRule, FileValidationRuleSets, VideoOrientationValue } from './file-validation-types';
|
|
3
3
|
export type { BlobUpload, BlobUploadStrategy, PreviewableUploadingFile, UploadPreviewMode, UploadingFile, UploadingFileStatus } from './upload-types';
|
|
4
|
-
export type { AddOptions, UploadMediaStoreOptions } from './upload-media-store.svelte';
|
|
4
|
+
export type { AddOptions, UploadMediaStoreOptions, UploadOutcome } from './upload-media-store.svelte';
|
|
5
5
|
export type { FileTypeCheck } from './file-helper';
|
|
6
|
-
export { toBlobWithName, toFileWithUploadUrl } from './
|
|
6
|
+
export { toBlobWithName, toFileWithUploadUrl } from './file-decorators';
|
|
7
7
|
export { uploadBlob } from './blob-storage';
|
|
8
8
|
export { openFile } from './open-file';
|
|
9
9
|
export { FileHelper } from './file-helper';
|
|
10
10
|
export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
|
|
11
11
|
export { AcceptFileType, isPreviewableType, matchesAcceptedFileTypes } from './file-types';
|
|
12
12
|
export { deriveAccept } from './file-validation-rule-sets';
|
|
13
|
-
export { resizeBlob, resizeImage } from './image-resizer';
|
|
13
|
+
export { DEFAULT_RESIZE_LIMITS, fitSize, resizeBlob, resizeImage, toPixelSize } from './image-resizer';
|
|
14
14
|
export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
15
15
|
export { FilesProvider } from './files-provider';
|
|
16
16
|
export { FileValidator } from './file-validator';
|
package/dist/core/files/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Utilities
|
|
2
|
-
export { toBlobWithName, toFileWithUploadUrl } from './
|
|
2
|
+
export { toBlobWithName, toFileWithUploadUrl } from './file-decorators';
|
|
3
3
|
export { uploadBlob } from './blob-storage';
|
|
4
4
|
export { openFile } from './open-file';
|
|
5
5
|
export { FileHelper } from './file-helper';
|
|
6
6
|
export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
|
|
7
7
|
export { AcceptFileType, isPreviewableType, matchesAcceptedFileTypes } from './file-types';
|
|
8
8
|
export { deriveAccept } from './file-validation-rule-sets';
|
|
9
|
-
export { resizeBlob, resizeImage } from './image-resizer';
|
|
9
|
+
export { DEFAULT_RESIZE_LIMITS, fitSize, resizeBlob, resizeImage, toPixelSize } from './image-resizer';
|
|
10
10
|
export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
11
11
|
export { FilesProvider } from './files-provider';
|
|
12
12
|
export { FileValidator } from './file-validator';
|
|
@@ -2,11 +2,23 @@ export type FileWithBlobUrl = File & {
|
|
|
2
2
|
blobUrl: string;
|
|
3
3
|
id: string;
|
|
4
4
|
};
|
|
5
|
+
export type SizedFile = File & {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
/** Two positive pixel limits not tied to width / height — the image's longer side fits the larger one, its shorter side the smaller. */
|
|
10
|
+
export type ResizeLimits = {
|
|
11
|
+
max1: number;
|
|
12
|
+
max2: number;
|
|
13
|
+
};
|
|
14
|
+
export type ResizedBlob = {
|
|
15
|
+
blob: Blob;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
5
19
|
export type FileWithUploadUrl<T extends File = File> = T & {
|
|
6
20
|
uploadUrl: string;
|
|
7
21
|
};
|
|
8
|
-
export declare const toFileWithUploadUrl: <T extends File>(file: T, uploadUrl: string) => FileWithUploadUrl<T>;
|
|
9
22
|
export type BlobWithName = Blob & {
|
|
10
23
|
name: string;
|
|
11
24
|
};
|
|
12
|
-
export declare const toBlobWithName: (blob: Blob, name: string) => BlobWithName;
|
package/dist/core/files/types.js
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
const fileWithUploadUrl = file;
|
|
3
|
-
fileWithUploadUrl.uploadUrl = uploadUrl;
|
|
4
|
-
return fileWithUploadUrl;
|
|
5
|
-
};
|
|
6
|
-
export const toBlobWithName = (blob, name) => {
|
|
7
|
-
const blobWithName = blob;
|
|
8
|
-
blobWithName.name = name;
|
|
9
|
-
return blobWithName;
|
|
10
|
-
};
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResizeLimits } from './types';
|
|
1
2
|
import type { BlobUploadStrategy, PreviewableUploadingFile, UploadPreviewMode, UploadingFile } from './upload-types';
|
|
2
3
|
type SuccessfulUpload = Extract<UploadingFile, {
|
|
3
4
|
status: 'success';
|
|
@@ -16,15 +17,8 @@ export type UploadOutcome = {
|
|
|
16
17
|
succeeded: SuccessfulUpload[];
|
|
17
18
|
};
|
|
18
19
|
export type UploadMediaStoreOptions = {
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
* defaults. `max1` / `max2` are not tied to width / height — the longer side is matched to
|
|
22
|
-
* the larger limit. @default `{ max1: 1600, max2: 1000 }`
|
|
23
|
-
*/
|
|
24
|
-
resize?: {
|
|
25
|
-
max1: number;
|
|
26
|
-
max2: number;
|
|
27
|
-
} | false;
|
|
20
|
+
/** Downscale images to these limits before upload; `false` uploads them as they are. @default DEFAULT_RESIZE_LIMITS (1600 / 1000) */
|
|
21
|
+
resize?: ResizeLimits | false;
|
|
28
22
|
/** Parallel upload limit. @default 20 */
|
|
29
23
|
concurrency?: number;
|
|
30
24
|
on?: {
|
|
@@ -62,7 +56,7 @@ export declare class UploadMediaStore {
|
|
|
62
56
|
private _inFlight;
|
|
63
57
|
private _nextRunId;
|
|
64
58
|
private _strategy;
|
|
65
|
-
private
|
|
59
|
+
private _opts;
|
|
66
60
|
constructor(strategy: BlobUploadStrategy, opts?: UploadMediaStoreOptions);
|
|
67
61
|
/** Reactive read of the queue. */
|
|
68
62
|
get files(): UploadingFile[];
|
|
@@ -102,12 +96,12 @@ export declare class UploadMediaStore {
|
|
|
102
96
|
* with no extra wiring.
|
|
103
97
|
*/
|
|
104
98
|
upload(): Promise<UploadOutcome>;
|
|
105
|
-
private
|
|
106
|
-
private
|
|
107
|
-
private
|
|
108
|
-
private
|
|
109
|
-
private
|
|
110
|
-
private
|
|
111
|
-
private
|
|
99
|
+
private _outcome;
|
|
100
|
+
private _settled;
|
|
101
|
+
private _runQueued;
|
|
102
|
+
private _maybeResize;
|
|
103
|
+
private _fail;
|
|
104
|
+
private _failAll;
|
|
105
|
+
private _transition;
|
|
112
106
|
}
|
|
113
107
|
export {};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { AppUploadActivity } from './app-upload-activity.svelte';
|
|
2
2
|
import { uploadBlob } from './blob-storage';
|
|
3
3
|
import { isPreviewableType } from './file-types';
|
|
4
|
-
import { resizeBlob } from './image-resizer';
|
|
4
|
+
import { DEFAULT_RESIZE_LIMITS, resizeBlob } from './image-resizer';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
import { default as pLimit } from 'p-limit';
|
|
7
|
-
const DEFAULT_RESIZE_LIMITS = { max1: 1600, max2: 1000 };
|
|
8
7
|
const DEFAULT_CONCURRENCY = 20;
|
|
9
8
|
const toPreviewUrl = (file, mode) => mode === 'always' || (mode === 'mime' && isPreviewableType(file.type)) ? URL.createObjectURL(file) : null;
|
|
10
9
|
const revokePreviewUrl = (file) => {
|
|
@@ -38,10 +37,10 @@ export class UploadMediaStore {
|
|
|
38
37
|
_inFlight = new Map();
|
|
39
38
|
_nextRunId = 0;
|
|
40
39
|
_strategy;
|
|
41
|
-
|
|
40
|
+
_opts;
|
|
42
41
|
constructor(strategy, opts = {}) {
|
|
43
42
|
this._strategy = strategy;
|
|
44
|
-
this.
|
|
43
|
+
this._opts = opts;
|
|
45
44
|
}
|
|
46
45
|
/** Reactive read of the queue. */
|
|
47
46
|
get files() {
|
|
@@ -89,19 +88,19 @@ export class UploadMediaStore {
|
|
|
89
88
|
async upload() {
|
|
90
89
|
const queued = this._files.filter((f) => f.status === 'queued' || f.status === 'error');
|
|
91
90
|
if (queued.length === 0) {
|
|
92
|
-
await this.
|
|
93
|
-
return this.
|
|
91
|
+
await this._settled();
|
|
92
|
+
return this._outcome();
|
|
94
93
|
}
|
|
95
94
|
// Claim before the first await, or a second upload() during the strategy call re-claims the same files.
|
|
96
95
|
for (const entry of queued) {
|
|
97
|
-
this.
|
|
96
|
+
this._transition(entry.id, (f) => ({ id: f.id, file: f.file, progress: 0, previewUrl: f.previewUrl, status: 'uploading' }));
|
|
98
97
|
}
|
|
99
98
|
const fileIds = new Set(queued.map((f) => f.id));
|
|
100
99
|
const activityRunId = AppUploadActivity.beginRun(() => this._files.filter((f) => fileIds.has(f.id)));
|
|
101
100
|
const runId = this._nextRunId++;
|
|
102
101
|
const run = (async () => {
|
|
103
102
|
try {
|
|
104
|
-
await this.
|
|
103
|
+
await this._runQueued(queued);
|
|
105
104
|
}
|
|
106
105
|
finally {
|
|
107
106
|
AppUploadActivity.endRun(activityRunId);
|
|
@@ -109,43 +108,43 @@ export class UploadMediaStore {
|
|
|
109
108
|
}
|
|
110
109
|
})();
|
|
111
110
|
this._inFlight.set(runId, run);
|
|
112
|
-
await this.
|
|
113
|
-
return this.
|
|
111
|
+
await this._settled();
|
|
112
|
+
return this._outcome();
|
|
114
113
|
}
|
|
115
|
-
|
|
114
|
+
_outcome() {
|
|
116
115
|
return {
|
|
117
116
|
failed: this._files.filter((f) => f.status === 'error'),
|
|
118
117
|
succeeded: this._files.filter((f) => f.status === 'success')
|
|
119
118
|
};
|
|
120
119
|
}
|
|
121
|
-
async
|
|
120
|
+
async _settled() {
|
|
122
121
|
while (this._inFlight.size > 0) {
|
|
123
122
|
await Promise.all([...this._inFlight.values()]);
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
|
-
async
|
|
125
|
+
async _runQueued(queued) {
|
|
127
126
|
let presigned;
|
|
128
127
|
try {
|
|
129
128
|
presigned = await this._strategy(queued.length);
|
|
130
129
|
}
|
|
131
130
|
catch (error) {
|
|
132
|
-
this.
|
|
131
|
+
this._failAll(queued, error);
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
134
|
if (presigned.length < queued.length) {
|
|
136
|
-
this.
|
|
135
|
+
this._failAll(queued, new Error(`BlobUploadStrategy returned ${presigned.length} slots for ${queued.length} files`));
|
|
137
136
|
return;
|
|
138
137
|
}
|
|
139
|
-
const limit = pLimit(this.
|
|
140
|
-
const resizeOpt = this.
|
|
138
|
+
const limit = pLimit(this._opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
139
|
+
const resizeOpt = this._opts.resize;
|
|
141
140
|
await Promise.all(queued.map((entry, i) => limit(async () => {
|
|
142
141
|
try {
|
|
143
|
-
const blob = resizeOpt === false ? entry.file : await this.
|
|
142
|
+
const blob = resizeOpt === false ? entry.file : await this._maybeResize(entry.file, resizeOpt ?? DEFAULT_RESIZE_LIMITS);
|
|
144
143
|
const slot = presigned[i];
|
|
145
144
|
await uploadBlob(slot.uploadUrl, blob, (loaded, total) => {
|
|
146
|
-
this.
|
|
145
|
+
this._transition(entry.id, (f) => (f.status === 'uploading' ? { ...f, progress: total > 0 ? loaded / total : 0 } : f));
|
|
147
146
|
});
|
|
148
|
-
const done = this.
|
|
147
|
+
const done = this._transition(entry.id, (f) => ({
|
|
149
148
|
id: f.id,
|
|
150
149
|
file: f.file,
|
|
151
150
|
progress: 1,
|
|
@@ -155,42 +154,40 @@ export class UploadMediaStore {
|
|
|
155
154
|
readUrl: slot.readUrl
|
|
156
155
|
}));
|
|
157
156
|
if (done?.status === 'success') {
|
|
158
|
-
this.
|
|
157
|
+
this._opts.on?.blobId?.(done, slot.id);
|
|
159
158
|
}
|
|
160
159
|
}
|
|
161
160
|
catch (error) {
|
|
162
|
-
this.
|
|
161
|
+
this._fail(entry.id, error);
|
|
163
162
|
}
|
|
164
163
|
})));
|
|
165
|
-
this.
|
|
164
|
+
this._opts.on?.done?.();
|
|
166
165
|
}
|
|
167
|
-
async
|
|
168
|
-
|
|
169
|
-
if (!file.type.startsWith('image/') || file.type === 'image/gif') {
|
|
166
|
+
async _maybeResize(file, limits) {
|
|
167
|
+
if (!file.type.startsWith('image/')) {
|
|
170
168
|
return file;
|
|
171
169
|
}
|
|
172
170
|
try {
|
|
173
|
-
return await resizeBlob(file, limits);
|
|
171
|
+
return (await resizeBlob(file, limits)).blob;
|
|
174
172
|
}
|
|
175
173
|
catch {
|
|
176
|
-
// Resize is best-effort: fall back to the original on failure (e.g. corrupt file).
|
|
177
174
|
return file;
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
|
-
|
|
177
|
+
_fail(id, error) {
|
|
181
178
|
const message = error instanceof Error ? error.message : 'Upload failed';
|
|
182
|
-
const failed = this.
|
|
179
|
+
const failed = this._transition(id, (f) => ({ id: f.id, file: f.file, progress: f.progress, previewUrl: f.previewUrl, status: 'error', error: message }));
|
|
183
180
|
if (failed?.status === 'error') {
|
|
184
|
-
this.
|
|
181
|
+
this._opts.on?.error?.(failed, error);
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
|
-
|
|
184
|
+
_failAll(files, error) {
|
|
188
185
|
for (const entry of files) {
|
|
189
|
-
this.
|
|
186
|
+
this._fail(entry.id, error);
|
|
190
187
|
}
|
|
191
|
-
this.
|
|
188
|
+
this._opts.on?.done?.();
|
|
192
189
|
}
|
|
193
|
-
|
|
190
|
+
_transition(id, next) {
|
|
194
191
|
let updated;
|
|
195
192
|
this._files = this._files.map((f) => {
|
|
196
193
|
if (f.id !== id) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { FileHelper } from '../../../core/files';
|
|
2
2
|
import { Dialog, DialogButton, DialogCancelButton } from '../../dialog';
|
|
3
3
|
import { Toastr } from '../../toast';
|
|
4
4
|
import { ImgCropper, ImgCropperToolbar, ImgCropperView } from '../img-cropper';
|
|
5
5
|
import { ImageEditorDialogLocalization } from './image-editor-dialog-localization';
|
|
6
|
+
import { default as mime } from 'mime';
|
|
7
|
+
import { nanoid } from 'nanoid';
|
|
6
8
|
import { untrack } from 'svelte';
|
|
7
9
|
const { controller, data } = $props();
|
|
8
10
|
const loc = new ImageEditorDialogLocalization();
|
|
@@ -12,22 +14,16 @@ const cropper = untrack(() => new ImgCropper({
|
|
|
12
14
|
fillColor: data.fillColor,
|
|
13
15
|
tools: data.tools
|
|
14
16
|
}));
|
|
15
|
-
const url = $derived(data.url);
|
|
16
17
|
const save = async () => {
|
|
17
18
|
try {
|
|
18
|
-
const result = await cropper.save();
|
|
19
|
-
// Closed mid-save: destroy() already ran, and a file made now would leak its blob URL.
|
|
20
|
-
if (!cropper.ready) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
19
|
+
const result = await cropper.save(data.resize);
|
|
23
20
|
if (!result) {
|
|
24
21
|
Toastr.error(loc.saveError);
|
|
25
22
|
return;
|
|
26
23
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
24
|
+
const baseName = data.fileName ? FileHelper.omitFileExtension(data.fileName) : nanoid(15);
|
|
25
|
+
const file = FileHelper.blobToFile(result.blob, `${baseName}.${mime.getExtension(result.blob.type)}`);
|
|
26
|
+
const croppedFile = Object.assign(file, { width: result.width, height: result.height });
|
|
31
27
|
controller.ok({ croppedFile, selectedRatio: cropper.aspectRatio });
|
|
32
28
|
}
|
|
33
29
|
catch {
|
|
@@ -50,7 +46,7 @@ $effect(() => untrack(() => {
|
|
|
50
46
|
{/snippet}
|
|
51
47
|
{#snippet bodySection()}
|
|
52
48
|
<div class="image-editor-dialog__workspace">
|
|
53
|
-
<ImgCropperView src={url} cropper={cropper}>
|
|
49
|
+
<ImgCropperView src={data.url} cropper={cropper}>
|
|
54
50
|
{#snippet toolbar()}
|
|
55
51
|
<ImgCropperToolbar cropper={cropper} />
|
|
56
52
|
{/snippet}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { ImageEditorDynamicAspectRatio, ImageEditorTool } from './types';
|
|
1
|
+
import { type ResizeLimits } from '../../../core/files';
|
|
2
|
+
import type { CroppedFile, ImageEditorDynamicAspectRatio, ImageEditorTool } from './types';
|
|
3
3
|
export type CropAndResizeOptions = {
|
|
4
4
|
/** Fixed aspect ratio, or dynamic selector, or omit to follow the picture's own. */
|
|
5
5
|
aspectRatio?: number | ImageEditorDynamicAspectRatio;
|
|
@@ -7,20 +7,13 @@ export type CropAndResizeOptions = {
|
|
|
7
7
|
mode?: 'contain' | 'cover';
|
|
8
8
|
/** Fill color for whatever the picture doesn't cover — `contain` gaps, free position, its own transparency (transparent by default). */
|
|
9
9
|
fillColor?: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
* height — the longer side matches the larger limit. Pass `false` to disable resize.
|
|
13
|
-
* @default `{ max1: 1600, max2: 1000 }`
|
|
14
|
-
*/
|
|
15
|
-
resize?: {
|
|
16
|
-
max1: number;
|
|
17
|
-
max2: number;
|
|
18
|
-
} | false;
|
|
10
|
+
/** Renders the cropped picture straight at a size that fits these limits; `false` keeps its natural resolution. @default DEFAULT_RESIZE_LIMITS (1600 / 1000) */
|
|
11
|
+
resize?: ResizeLimits | false;
|
|
19
12
|
/** Actions that rework the picture itself (e.g. background removal), one toolbar button each. */
|
|
20
13
|
tools?: ImageEditorTool[];
|
|
21
14
|
};
|
|
22
15
|
/**
|
|
23
|
-
* Opens the kit's image editor dialog
|
|
16
|
+
* Opens the kit's image editor dialog and returns the cropped picture, rendered at a size that fits `resize`.
|
|
24
17
|
* Returns `null` when the user cancels the dialog.
|
|
25
18
|
*/
|
|
26
|
-
export declare const cropAndResizeImage: (file: File, options?: CropAndResizeOptions) => Promise<
|
|
19
|
+
export declare const cropAndResizeImage: (file: File, options?: CropAndResizeOptions) => Promise<CroppedFile | null>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_RESIZE_LIMITS } from '../../../core/files';
|
|
2
2
|
import { openImageEditorDialog } from './open-image-editor-dialog';
|
|
3
|
-
const DEFAULT_RESIZE_LIMITS = { max1: 1600, max2: 1000 };
|
|
4
3
|
/**
|
|
5
|
-
* Opens the kit's image editor dialog
|
|
4
|
+
* Opens the kit's image editor dialog and returns the cropped picture, rendered at a size that fits `resize`.
|
|
6
5
|
* Returns `null` when the user cancels the dialog.
|
|
7
6
|
*/
|
|
8
7
|
export const cropAndResizeImage = async (file, options = {}) => {
|
|
@@ -10,25 +9,14 @@ export const cropAndResizeImage = async (file, options = {}) => {
|
|
|
10
9
|
try {
|
|
11
10
|
const result = await openImageEditorDialog({
|
|
12
11
|
url: sourceUrl,
|
|
12
|
+
fileName: file.name,
|
|
13
13
|
aspectRatio: options.aspectRatio,
|
|
14
14
|
mode: options.mode,
|
|
15
15
|
fillColor: options.fillColor,
|
|
16
|
-
tools: options.tools
|
|
16
|
+
tools: options.tools,
|
|
17
|
+
resize: options.resize === false ? undefined : (options.resize ?? DEFAULT_RESIZE_LIMITS)
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const cropped = result.value.croppedFile;
|
|
22
|
-
if (options.resize === false) {
|
|
23
|
-
return cropped;
|
|
24
|
-
}
|
|
25
|
-
const limits = options.resize ?? DEFAULT_RESIZE_LIMITS;
|
|
26
|
-
const resizedBlob = await resizeBlob(cropped, limits);
|
|
27
|
-
if (resizedBlob === cropped) {
|
|
28
|
-
return cropped;
|
|
29
|
-
}
|
|
30
|
-
FileWithBlobDataHelper.dispose(cropped.blobUrl);
|
|
31
|
-
return FileWithBlobDataHelper.fromBlob(resizedBlob, { id: cropped.id });
|
|
19
|
+
return result.wasCanceled ? null : result.value.croppedFile;
|
|
32
20
|
}
|
|
33
21
|
finally {
|
|
34
22
|
URL.revokeObjectURL(sourceUrl);
|
|
@@ -18,8 +18,13 @@ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types'
|
|
|
18
18
|
* - `tools` — one toolbar button per tool; `run` gets the picture as shown — cropped and rotated, at its natural
|
|
19
19
|
* resolution — and its result replaces it (`null` leaves it as it is). Reset returns to the source.
|
|
20
20
|
*
|
|
21
|
+
* ### Size
|
|
22
|
+
* - `resize` — the saved picture is rendered straight at a size that fits these limits (one encode, no upscale past
|
|
23
|
+
* them). Omitted — its natural resolution.
|
|
24
|
+
*
|
|
21
25
|
* ### Result
|
|
22
|
-
* - `croppedFile` — `
|
|
26
|
+
* - `croppedFile` — a plain `File` with its `width`/`height` — natural resolution, or fitted to `resize` when given; no
|
|
27
|
+
* object URL is created for it. Named after `fileName` with the saved picture's extension, or randomly without it.
|
|
23
28
|
* - `selectedRatio` — the active aspect ratio at save time (`null` when following the picture's own).
|
|
24
29
|
*/
|
|
25
30
|
export declare const openImageEditorDialog: (options: ImageEditorDialogOptions) => Promise<DialogResult<ImageEditorDialogResult>>;
|
|
@@ -18,8 +18,13 @@ import { default as ImageEditorDialog } from './cmp.image-editor-dialog.svelte';
|
|
|
18
18
|
* - `tools` — one toolbar button per tool; `run` gets the picture as shown — cropped and rotated, at its natural
|
|
19
19
|
* resolution — and its result replaces it (`null` leaves it as it is). Reset returns to the source.
|
|
20
20
|
*
|
|
21
|
+
* ### Size
|
|
22
|
+
* - `resize` — the saved picture is rendered straight at a size that fits these limits (one encode, no upscale past
|
|
23
|
+
* them). Omitted — its natural resolution.
|
|
24
|
+
*
|
|
21
25
|
* ### Result
|
|
22
|
-
* - `croppedFile` — `
|
|
26
|
+
* - `croppedFile` — a plain `File` with its `width`/`height` — natural resolution, or fitted to `resize` when given; no
|
|
27
|
+
* object URL is created for it. Named after `fileName` with the saved picture's extension, or randomly without it.
|
|
23
28
|
* - `selectedRatio` — the active aspect ratio at save time (`null` when following the picture's own).
|
|
24
29
|
*/
|
|
25
30
|
export const openImageEditorDialog = async (options) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResizeLimits, SizedFile } from '../../../core/files';
|
|
2
2
|
export type ImageEditorDynamicAspectRatio = {
|
|
3
3
|
initial: number;
|
|
4
4
|
supported: number[];
|
|
@@ -23,17 +23,20 @@ export type ImageEditorTool = {
|
|
|
23
23
|
export type ImageEditorDialogOptions = {
|
|
24
24
|
/** The picture to edit — same-origin, a `blob:` / `data:` URL, or a server that sends CORS headers; anything else fails to load. */
|
|
25
25
|
url: string;
|
|
26
|
+
/** Name of the saved file; its extension follows the saved picture's type. Omitted — a random name. */
|
|
27
|
+
fileName?: string;
|
|
28
|
+
/** Fill color for whatever the picture doesn't cover — `contain` gaps, free position, its own transparency (transparent by default). */
|
|
26
29
|
fillColor?: string;
|
|
27
30
|
/** @default 'cover' */
|
|
28
31
|
mode?: 'contain' | 'cover';
|
|
32
|
+
/** Fixed aspect ratio, or dynamic selector, or omit to follow the picture's own. */
|
|
29
33
|
aspectRatio?: number | ImageEditorDynamicAspectRatio;
|
|
30
34
|
/** Actions that rework the picture itself (e.g. background removal), one toolbar button each. */
|
|
31
35
|
tools?: ImageEditorTool[];
|
|
36
|
+
/** Renders the saved picture straight at a size that fits these limits. Omitted — its natural resolution. */
|
|
37
|
+
resize?: ResizeLimits;
|
|
32
38
|
};
|
|
33
|
-
export type CroppedFile =
|
|
34
|
-
width: number;
|
|
35
|
-
height: number;
|
|
36
|
-
};
|
|
39
|
+
export type CroppedFile = SizedFile;
|
|
37
40
|
export type ImageEditorDialogResult = {
|
|
38
41
|
croppedFile: CroppedFile;
|
|
39
42
|
selectedRatio: number | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResizeLimits } from '../../../core/files';
|
|
1
2
|
import { type FrameFitMode, type Point, type Rect, type Size } from './frame-geometry';
|
|
2
3
|
export type CropperMode = FrameFitMode;
|
|
3
4
|
export type { Point, Rect };
|
|
@@ -135,8 +136,11 @@ export declare class ImgCropper {
|
|
|
135
136
|
* along and keeps what's inside it; a fixed-aspect frame can't turn, so the picture is laid out afresh instead. */
|
|
136
137
|
rotate: (direction: 1 | -1) => void;
|
|
137
138
|
reset: () => void;
|
|
138
|
-
/**
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Renders the frame to an image, applying a pending selection first, downscaled to fit `limits` when given;
|
|
141
|
+
* `null` when there's nothing to render or the browser can't.
|
|
142
|
+
*/
|
|
143
|
+
save: (limits?: ResizeLimits) => Promise<ImgCropperSaveResult | null>;
|
|
140
144
|
/**
|
|
141
145
|
* Where the whole turned source lands when the picture is drawn into `picture` — its center and its size on each axis,
|
|
142
146
|
* before `rotation` swaps them. The view and `save()` both place the source through this, so they can't drift apart.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fitSize, toPixelSize } from '../../../core/files';
|
|
1
2
|
import { ColorHelper, Utils } from '../../../core/utils';
|
|
2
3
|
import { alignBox, boxAtZoom, boxTurnedWithFrame, clampBox, containBox, containZoom, coverBox, fitFrame, MAX_FRAME_ZOOM, MIN_FREE_ZOOM, moveBox, scaleBox, selectionFromDrag, shiftSelection, zoomOf } from './frame-geometry';
|
|
3
4
|
import { Picture } from './picture';
|
|
@@ -403,14 +404,17 @@ export class ImgCropper {
|
|
|
403
404
|
this.aspectRatio = this._initialAspectRatio;
|
|
404
405
|
this.layout();
|
|
405
406
|
};
|
|
406
|
-
/**
|
|
407
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Renders the frame to an image, applying a pending selection first, downscaled to fit `limits` when given;
|
|
409
|
+
* `null` when there's nothing to render or the browser can't.
|
|
410
|
+
*/
|
|
411
|
+
save = async (limits) => {
|
|
408
412
|
if (this.hasSelection) {
|
|
409
413
|
this.applyCrop();
|
|
410
414
|
}
|
|
411
415
|
this._processing = true;
|
|
412
416
|
try {
|
|
413
|
-
const rendered = this._render();
|
|
417
|
+
const rendered = this._render(limits);
|
|
414
418
|
if (!rendered) {
|
|
415
419
|
return null;
|
|
416
420
|
}
|
|
@@ -441,7 +445,7 @@ export class ImgCropper {
|
|
|
441
445
|
size: { w: turned.w * perX, h: turned.h * perY }
|
|
442
446
|
};
|
|
443
447
|
};
|
|
444
|
-
_render = () => {
|
|
448
|
+
_render = (limits) => {
|
|
445
449
|
const source = this._current;
|
|
446
450
|
const { box, frame, croppedSize } = this;
|
|
447
451
|
if (this.failed || !source || !box || !frame || !croppedSize) {
|
|
@@ -450,13 +454,16 @@ export class ImgCropper {
|
|
|
450
454
|
const nativeScale = croppedSize.w / box.w;
|
|
451
455
|
const nativeArea = frame.w * frame.h * nativeScale * nativeScale;
|
|
452
456
|
const areaLimit = Math.min(croppedSize.w * croppedSize.h * MAX_OUTPUT_AREA_UPSCALE, MAX_OUTPUT_PIXELS);
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
const height =
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
const
|
|
457
|
+
const cappedScale = nativeArea > areaLimit ? nativeScale * Math.sqrt(areaLimit / nativeArea) : nativeScale;
|
|
458
|
+
const cappedSize = { width: frame.w * cappedScale, height: frame.h * cappedScale };
|
|
459
|
+
const { width, height } = limits ? fitSize(cappedSize, limits) : toPixelSize(cappedSize);
|
|
460
|
+
// Per axis from the whole-pixel canvas: one shared scale rounds a frame-wide box a pixel past the canvas edge.
|
|
461
|
+
const scaleX = width / frame.w;
|
|
462
|
+
const scaleY = height / frame.h;
|
|
463
|
+
const left = Math.round(box.x * scaleX);
|
|
464
|
+
const top = Math.round(box.y * scaleY);
|
|
465
|
+
const right = Math.round((box.x + box.w) * scaleX);
|
|
466
|
+
const bottom = Math.round((box.y + box.h) * scaleY);
|
|
460
467
|
const canvas = document.createElement('canvas');
|
|
461
468
|
canvas.width = width;
|
|
462
469
|
canvas.height = height;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,18 +9,15 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "vite dev --port 3011 --host",
|
|
12
|
-
"build": "svelte-package --tsconfig ./tsconfig.app.json && npm run build:css
|
|
12
|
+
"build": "npm run check && svelte-package --tsconfig ./tsconfig.app.json && npm run build:css",
|
|
13
13
|
"build:css": "sass dist/styles/_normalize.scss dist/styles/normalize.css --no-source-map && sass dist/styles/_reset.scss dist/styles/reset.css --no-source-map",
|
|
14
14
|
"publish:prod": "npm run build && npm publish --access public --tag latest",
|
|
15
15
|
"version:dev": "node -e \"const p=require('./package.json');p.version=p.version.split('-')[0]+'-'+Date.now();require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"",
|
|
16
16
|
"publish:dev": "npm run build && npm run version:dev && npm publish --tag next && git checkout -- package.json",
|
|
17
17
|
"pack": "npm run build && npm pack",
|
|
18
18
|
"preview": "vite preview",
|
|
19
|
-
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
|
20
|
-
"
|
|
21
|
-
"lint": "prettier --check --plugin prettier-plugin-svelte . && eslint .",
|
|
22
|
-
"lint-format": "prettier --write --plugin prettier-plugin-svelte . && eslint --fix .",
|
|
23
|
-
"format": "prettier --write --plugin prettier-plugin-svelte ."
|
|
19
|
+
"check": "svelte-check --tsconfig ./tsconfig.app.json --fail-on-warnings --output machine && tsc -p tsconfig.node.json && eslint . --format codeframe --max-warnings 0 && prettier --check --plugin prettier-plugin-svelte --cache .",
|
|
20
|
+
"lint:fix": "eslint --fix . && prettier --write --plugin prettier-plugin-svelte ."
|
|
24
21
|
},
|
|
25
22
|
"files": [
|
|
26
23
|
"dist"
|