@streamscloud/kit 0.11.2 → 0.11.3

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.
@@ -1,3 +1,4 @@
1
+ import type { VideoOrientationValue } from './file-validation-types';
1
2
  export declare class FileValidationLocalization {
2
3
  get invalidFile(): string;
3
4
  get unsupportedType(): string;
@@ -8,5 +9,7 @@ export declare class FileValidationLocalization {
8
9
  get minDimensions(): (width: number, height: number) => string;
9
10
  get maxDuration(): (seconds: number) => string;
10
11
  get aspectRatio(): (ratios: string) => string;
12
+ get aspectRatioRange(): (min: string, max: string) => string;
13
+ get orientation(): (orientation: VideoOrientationValue) => string;
11
14
  get maxBitrate(): (kbps: number) => string;
12
15
  }
@@ -27,10 +27,20 @@ export class FileValidationLocalization {
27
27
  get aspectRatio() {
28
28
  return loc.aspectRatio[AppLocale.current];
29
29
  }
30
+ get aspectRatioRange() {
31
+ return loc.aspectRatioRange[AppLocale.current];
32
+ }
33
+ get orientation() {
34
+ return loc.orientation[AppLocale.current];
35
+ }
30
36
  get maxBitrate() {
31
37
  return loc.maxBitrate[AppLocale.current];
32
38
  }
33
39
  }
40
+ const orientationLabels = {
41
+ en: { landscape: 'landscape', portrait: 'portrait', square: 'square' },
42
+ no: { landscape: 'liggende', portrait: 'stående', square: 'kvadratisk' }
43
+ };
34
44
  const loc = {
35
45
  invalidFile: {
36
46
  en: 'Invalid file',
@@ -68,6 +78,30 @@ const loc = {
68
78
  en: (ratios) => `Allowed aspect ratio: ${ratios}`,
69
79
  no: (ratios) => `Tillatt sideforhold: ${ratios}`
70
80
  },
81
+ aspectRatioRange: {
82
+ en: (min, max) => {
83
+ if (min && max) {
84
+ return `Allowed aspect ratio: ${min}–${max}`;
85
+ }
86
+ if (min) {
87
+ return `Allowed aspect ratio: ${min} or wider`;
88
+ }
89
+ return `Allowed aspect ratio: up to ${max}`;
90
+ },
91
+ no: (min, max) => {
92
+ if (min && max) {
93
+ return `Tillatt sideforhold: ${min}–${max}`;
94
+ }
95
+ if (min) {
96
+ return `Tillatt sideforhold: ${min} eller bredere`;
97
+ }
98
+ return `Tillatt sideforhold: opptil ${max}`;
99
+ }
100
+ },
101
+ orientation: {
102
+ en: (orientation) => `Allowed orientation: ${orientationLabels.en[orientation]}`,
103
+ no: (orientation) => `Tillatt retning: ${orientationLabels.no[orientation]}`
104
+ },
71
105
  maxBitrate: {
72
106
  en: (kbps) => `Maximum bitrate: ${kbps} kbps`,
73
107
  no: (kbps) => `Maks bitrate: ${kbps} kbps`
@@ -1,4 +1,4 @@
1
- import type { FileValidationRule } from './file-validation-types';
1
+ import type { AspectRatioBound, FileValidationRule, VideoOrientationValue } from './file-validation-types';
2
2
  /**
3
3
  * Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
4
4
  * rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
@@ -31,6 +31,17 @@ export declare const FileRules: {
31
31
  MaxSize: (bytes: number, message?: string) => FileValidationRule;
32
32
  Mime: (accept: string, message?: string) => FileValidationRule;
33
33
  VideoAspectRatio: (ratios: string[], message?: string, tolerancePercent?: number) => FileValidationRule;
34
+ /**
35
+ * Validates a video's width / height ratio against an inclusive `[min, max]` interval. Each
36
+ * bound is a `'w:h'` string or a decimal; both are optional, so the interval can be open-ended
37
+ * (`{ min }` = "this wide or wider", `{ max }` = "up to this"). `tolerancePercent` widens the
38
+ * interval outward on both edges. An empty `{}` specifies no constraint and accepts everything.
39
+ */
40
+ VideoAspectRatioRange: (range: {
41
+ min?: AspectRatioBound;
42
+ max?: AspectRatioBound;
43
+ }, message?: string, tolerancePercent?: number) => FileValidationRule;
44
+ VideoOrientation: (orientation: VideoOrientationValue, message?: string, tolerancePercent?: number) => FileValidationRule;
34
45
  VideoDimensions: (max: {
35
46
  width: number;
36
47
  height: number;
@@ -43,10 +43,25 @@ const readVideoDimensions = (file) => new Promise((resolve, reject) => {
43
43
  };
44
44
  video.src = url;
45
45
  });
46
- const parseRatio = (ratio) => {
47
- const [w, h] = ratio.split(':').map((part) => parseFloat(part));
48
- return h ? w / h : NaN;
46
+ const parseRatio = (value) => {
47
+ if (typeof value === 'number') {
48
+ return Number.isFinite(value) && value > 0 ? value : NaN;
49
+ }
50
+ if (value.includes(':')) {
51
+ const [w, h] = value.split(':').map((part) => parseFloat(part));
52
+ return h ? w / h : NaN;
53
+ }
54
+ const num = parseFloat(value);
55
+ return num > 0 ? num : NaN;
56
+ };
57
+ const resolveBound = (value) => {
58
+ if (value === undefined) {
59
+ return null;
60
+ }
61
+ const ratio = parseRatio(value);
62
+ return Number.isFinite(ratio) ? ratio : null;
49
63
  };
64
+ const formatBound = (value) => (value === undefined ? '' : typeof value === 'number' ? String(value) : value);
50
65
  const formatExtensions = (extensions) => extensions.map((ext) => '.' + ext.replace(/^\./, '')).join(', ');
51
66
  const messages = new FileValidationLocalization();
52
67
  /**
@@ -113,6 +128,35 @@ export const FileRules = {
113
128
  });
114
129
  return matches ? null : (message ?? messages.aspectRatio(ratios.join(', ')));
115
130
  },
131
+ /**
132
+ * Validates a video's width / height ratio against an inclusive `[min, max]` interval. Each
133
+ * bound is a `'w:h'` string or a decimal; both are optional, so the interval can be open-ended
134
+ * (`{ min }` = "this wide or wider", `{ max }` = "up to this"). `tolerancePercent` widens the
135
+ * interval outward on both edges. An empty `{}` specifies no constraint and accepts everything.
136
+ */
137
+ VideoAspectRatioRange: (range, message, tolerancePercent = 1) => async (file) => {
138
+ const { width, height } = await readVideoDimensions(file);
139
+ if (!width || !height) {
140
+ return message ?? messages.aspectRatioRange(formatBound(range.min), formatBound(range.max));
141
+ }
142
+ const actual = width / height;
143
+ const lo = resolveBound(range.min);
144
+ const hi = resolveBound(range.max);
145
+ const factor = 1 + tolerancePercent / 100;
146
+ const okLo = lo === null || actual >= lo / factor;
147
+ const okHi = hi === null || actual <= hi * factor;
148
+ return okLo && okHi ? null : (message ?? messages.aspectRatioRange(formatBound(range.min), formatBound(range.max)));
149
+ },
150
+ VideoOrientation: (orientation, message, tolerancePercent = 1) => async (file) => {
151
+ const { width, height } = await readVideoDimensions(file);
152
+ if (!width || !height) {
153
+ return message ?? messages.orientation(orientation);
154
+ }
155
+ const actual = width / height;
156
+ const margin = tolerancePercent / 100;
157
+ const ok = orientation === 'landscape' ? actual > 1 + margin : orientation === 'portrait' ? actual < 1 - margin : Math.abs(actual - 1) <= margin;
158
+ return ok ? null : (message ?? messages.orientation(orientation));
159
+ },
116
160
  VideoDimensions: (max, message) => async (file) => {
117
161
  const { width, height } = await readVideoDimensions(file);
118
162
  return width > max.width || height > max.height ? (message ?? messages.maxDimensions(max.width, max.height)) : null;
@@ -23,3 +23,7 @@ export type FileValidationResult = {
23
23
  isValid: boolean;
24
24
  errors: string[];
25
25
  };
26
+ /** Aspect-ratio bound: a `'w:h'` string (`'16:9'`) or a plain decimal (`1.78`). */
27
+ export type AspectRatioBound = string | number;
28
+ /** Video orientation class, derived from the width / height ratio. */
29
+ export type VideoOrientationValue = 'landscape' | 'portrait' | 'square';
@@ -1,5 +1,5 @@
1
1
  export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl } from './types';
2
- export type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from './file-validation-types';
2
+ export type { AspectRatioBound, FileValidationResult, FileValidationRule, FileValidationRuleSets, VideoOrientationValue } from './file-validation-types';
3
3
  export type { BlobKind, BlobKinds, BlobUpload, BlobUploadStrategy, UploadingFile, UploadingFileStatus } from './upload-types';
4
4
  export type { UploadMediaStoreOptions } from './upload-media-store.svelte';
5
5
  export { toBlobWithName, toFileWithUploadUrl } from './types';
@@ -146,8 +146,8 @@ playable (preserves card height). Optional `duration` overlay renders bottom-rig
146
146
  }
147
147
  .grid-card-media__duration-anchor {
148
148
  position: absolute;
149
- bottom: 0.5rem;
150
- right: 0.5rem;
149
+ bottom: 0.375rem;
150
+ right: 0.375rem;
151
151
  z-index: 1;
152
152
  }
153
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",