@design-edito/tools 0.5.14 → 0.5.16
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/agnostic/arrays/index.d.ts +2 -2
- package/agnostic/arrays/index.js +2 -2
- package/agnostic/colors/index.d.ts +4 -4
- package/agnostic/colors/index.js +4 -4
- package/agnostic/css/index.d.ts +2 -2
- package/agnostic/css/index.js +2 -2
- package/agnostic/errors/index.d.ts +1 -1
- package/agnostic/errors/index.js +1 -1
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +15 -15
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +15 -15
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +4 -4
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +4 -4
- package/agnostic/html/index.d.ts +2 -2
- package/agnostic/html/index.js +2 -2
- package/agnostic/index.d.ts +2 -2
- package/agnostic/index.js +2 -2
- package/agnostic/misc/index.d.ts +5 -5
- package/agnostic/misc/index.js +5 -5
- package/agnostic/numbers/index.d.ts +2 -2
- package/agnostic/numbers/index.js +2 -2
- package/agnostic/objects/index.d.ts +2 -2
- package/agnostic/objects/index.js +2 -2
- package/agnostic/random/index.d.ts +1 -1
- package/agnostic/random/index.js +1 -1
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/index.js +1 -1
- package/agnostic/subtitles/index.d.ts +1 -1
- package/agnostic/subtitles/index.js +1 -1
- package/agnostic/time/dates/format-date/index.d.ts +19 -4
- package/agnostic/time/dates/format-date/index.js +24 -7
- package/agnostic/time/dates/format-date/index.test.js +20 -7
- package/agnostic/time/dates/format-date/parts.js +4 -4
- package/agnostic/time/dates/format-date/types.d.ts +9 -9
- package/agnostic/time/duration/format-duration/index.d.ts +18 -1
- package/agnostic/time/duration/format-duration/index.js +44 -7
- package/agnostic/time/duration/format-duration/index.test.js +27 -0
- package/agnostic/time/duration/format-duration/types.d.ts +21 -1
- package/agnostic/time/index.d.ts +1 -1
- package/agnostic/time/index.js +1 -1
- package/components/Video/index.controlled.d.ts +22 -2
- package/components/Video/index.controlled.js +21 -5
- package/components/Video/index.d.ts +13 -1
- package/components/Video/index.js +18 -2
- package/components/Video/utils.d.ts +0 -31
- package/components/Video/utils.js +0 -58
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@google-cloud/storage/file/index.d.ts +4 -4
- package/node/@google-cloud/storage/file/index.js +4 -4
- package/node/cloud-storage/operations/index.d.ts +2 -2
- package/node/cloud-storage/operations/index.js +2 -2
- package/node/encryption/@aes-256-gcm/index.d.ts +1 -1
- package/node/encryption/@aes-256-gcm/index.js +1 -1
- package/node/files/index.d.ts +1 -1
- package/node/files/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +3 -3
- package/node/images/transform/operations/index.js +3 -3
- package/node/process/index.d.ts +1 -1
- package/node/process/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/** Every token `formatDate` substitutes in a template. */
|
|
2
|
-
export type DateToken = 'D' | 'DD' | 'd' | 'dd' | 'M' | 'MM' | 'MMM' | 'MMMM' | 'YY' | 'YYYY' | 'H' | 'HH' | 'h' | 'hh' | 'm' | 'mm' | 's' | 'ss' | 'ms' | 'A' | 'a' | 'th';
|
|
2
|
+
export type DateToken = 'D' | 'DD' | 'd' | 'dd' | 'ddd' | 'dddd' | 'M' | 'MM' | 'MMM' | 'MMMM' | 'YY' | 'YYYY' | 'H' | 'HH' | 'h' | 'hh' | 'm' | 'mm' | 's' | 'ss' | 'ms' | 'A' | 'a' | 'th';
|
|
3
3
|
/**
|
|
4
4
|
* A date's parts, keyed by unit token.
|
|
5
5
|
*
|
|
6
6
|
* Quantities are **numbers** — a day of month is `1`, not `'01'`. Padding and
|
|
7
7
|
* truncation are rendering concerns and belong to `formatDate`, which is why the
|
|
8
|
-
* `DD`, `MM`, `YY`, `HH`, `hh`, `mm` and `ss` tokens have no entry
|
|
9
|
-
* render the same quantity as
|
|
10
|
-
* text rather than a quantity stay strings.
|
|
8
|
+
* `d`, `dd`, `DD`, `MM`, `YY`, `HH`, `hh`, `mm` and `ss` tokens have no entry
|
|
9
|
+
* here: they render the same quantity as the bare token they double. The parts
|
|
10
|
+
* that are localised text rather than a quantity stay strings.
|
|
11
11
|
*/
|
|
12
12
|
export type DateTokenParts = {
|
|
13
|
-
/** Day of month, 1–31. */
|
|
13
|
+
/** Day of month, 1–31. Rendered by `D`, `DD`, `d` and `dd` alike. */
|
|
14
14
|
D: number;
|
|
15
15
|
/** Short weekday name, localised. */
|
|
16
|
-
|
|
16
|
+
ddd: string;
|
|
17
17
|
/** Full weekday name, localised. */
|
|
18
|
-
|
|
18
|
+
dddd: string;
|
|
19
19
|
/** Month number, 1–12 — January is `1`, not `0`. */
|
|
20
20
|
M: number;
|
|
21
21
|
/** Short month name, localised. */
|
|
@@ -51,9 +51,9 @@ export type DateTokenParts = {
|
|
|
51
51
|
export type NamedDateParts = {
|
|
52
52
|
/** {@link DateTokenParts.D} */
|
|
53
53
|
dayOfMonth: number;
|
|
54
|
-
/** {@link DateTokenParts.
|
|
54
|
+
/** {@link DateTokenParts.ddd} */
|
|
55
55
|
shortWeekdayName: string;
|
|
56
|
-
/** {@link DateTokenParts.
|
|
56
|
+
/** {@link DateTokenParts.dddd} */
|
|
57
57
|
fullWeekdayName: string;
|
|
58
58
|
/** {@link DateTokenParts.M} */
|
|
59
59
|
monthNumber: number;
|
|
@@ -19,14 +19,31 @@ import type { FormatDurationOptions } from './types.js';
|
|
|
19
19
|
* - `m` / `mm` : Minutes
|
|
20
20
|
* - `s` / `ss` : Seconds
|
|
21
21
|
* - `ms` : Milliseconds, padded to three digits
|
|
22
|
+
* - `f` / `ff` : The millisecond part counted in frames, at `options.fps`
|
|
22
23
|
*
|
|
23
24
|
* @param duration - The duration to format, as a {@link Duration} or a number of
|
|
24
25
|
* milliseconds.
|
|
25
26
|
* @param format - The template string containing tokens.
|
|
26
|
-
* @param options - How to handle the remainder
|
|
27
|
+
* @param options - How to handle the remainder, the month/year approximation and
|
|
28
|
+
* the frame rate.
|
|
27
29
|
* @returns Formatted duration string. An unknown token is left untouched, braces
|
|
28
30
|
* included.
|
|
29
31
|
* @see {@link getDurationParts} to reach the same parts as plain values.
|
|
32
|
+
* @see {@link formatDate} — the same template grammar, for a point in time rather
|
|
33
|
+
* than a length of it.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* This function and `formatDate` share one grammar: `{{…}}` delimiters, everything
|
|
37
|
+
* outside them literal, the bare token being the plain number and the doubled one
|
|
38
|
+
* that same number padded to two digits, `ms` padded to three, and an unknown token
|
|
39
|
+
* left as written. No token means two different kinds of thing across the two.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* **The template decides the breakdown, so nothing is ever lost for not having been
|
|
43
|
+
* asked for.** `'{{w}}w {{h}}h'` counts hours up to 167 because the week is there and
|
|
44
|
+
* the day is not; `'{{mm}}:{{ss}}'` on an hour-long video reads `62:05` rather than
|
|
45
|
+
* dropping the hour. The single-unit case follows from the same rule: `'{{s}}s'`
|
|
46
|
+
* carries the whole duration.
|
|
30
47
|
*
|
|
31
48
|
* @remarks
|
|
32
49
|
* A negative duration yields negative parts, so every token renders a signed
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { getDurationParts } from './parts.js';
|
|
2
2
|
// Alternation is ordered, and the first branch that fits wins — so the longest
|
|
3
3
|
// tokens come first. `m` ahead of `ms` or `mm` would swallow them.
|
|
4
|
-
const tokenRegexp = /\{\{(YY|MM|ww|dd|hh|mm|ss|ms|Y|M|w|d|h|m|s)\}\}/gv;
|
|
5
|
-
/**
|
|
4
|
+
const tokenRegexp = /\{\{(YY|MM|ww|dd|hh|mm|ss|ms|ff|Y|M|w|d|h|m|s|f)\}\}/gv;
|
|
5
|
+
/**
|
|
6
|
+
* Which tokens read which unit, longest unit first.
|
|
7
|
+
*
|
|
8
|
+
* `f` and `ff` sit under `ms` rather than under a unit of their own: a frame is
|
|
9
|
+
* the millisecond part counted in another base, so a template asking for frames
|
|
10
|
+
* is a template asking the cascade to run down to milliseconds.
|
|
11
|
+
*/
|
|
6
12
|
const unitTokens = [
|
|
7
13
|
['Y', ['Y', 'YY']],
|
|
8
14
|
['M', ['M', 'MM']],
|
|
@@ -11,7 +17,7 @@ const unitTokens = [
|
|
|
11
17
|
['h', ['h', 'hh']],
|
|
12
18
|
['m', ['m', 'mm']],
|
|
13
19
|
['s', ['s', 'ss']],
|
|
14
|
-
['ms', ['ms']]
|
|
20
|
+
['ms', ['ms', 'f', 'ff']]
|
|
15
21
|
];
|
|
16
22
|
function pad(value, length) {
|
|
17
23
|
return `${value}`.padStart(length, '0');
|
|
@@ -36,8 +42,22 @@ const renderers = {
|
|
|
36
42
|
'mm': parts => pad(parts.m, 2),
|
|
37
43
|
's': parts => `${parts.s}`,
|
|
38
44
|
'ss': parts => pad(parts.s, 2),
|
|
39
|
-
'ms': parts => pad(parts.ms, 3)
|
|
45
|
+
'ms': parts => pad(parts.ms, 3),
|
|
46
|
+
'f': (parts, fps) => `${toFrames(parts.ms, fps)}`,
|
|
47
|
+
'ff': (parts, fps) => pad(toFrames(parts.ms, fps), 2)
|
|
40
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* The millisecond part counted in frames.
|
|
51
|
+
*
|
|
52
|
+
* Truncated rather than rounded: a frame is only reached once it has fully
|
|
53
|
+
* elapsed, so 39 ms at 25 fps is frame `0`, not frame `1`. The sign is carried
|
|
54
|
+
* around the truncation, so a negative part stays negative — the whole function
|
|
55
|
+
* keeps a duration's parts faithful rather than tidying them.
|
|
56
|
+
*/
|
|
57
|
+
function toFrames(ms, fps) {
|
|
58
|
+
const sign = ms < 0 ? -1 : 1;
|
|
59
|
+
return Math.floor(Math.abs(ms) / 1000 * fps) * sign;
|
|
60
|
+
}
|
|
41
61
|
/** The units a template asks for, read off the tokens it actually uses. */
|
|
42
62
|
function unitsInFormat(format) {
|
|
43
63
|
return unitTokens
|
|
@@ -63,14 +83,31 @@ function unitsInFormat(format) {
|
|
|
63
83
|
* - `m` / `mm` : Minutes
|
|
64
84
|
* - `s` / `ss` : Seconds
|
|
65
85
|
* - `ms` : Milliseconds, padded to three digits
|
|
86
|
+
* - `f` / `ff` : The millisecond part counted in frames, at `options.fps`
|
|
66
87
|
*
|
|
67
88
|
* @param duration - The duration to format, as a {@link Duration} or a number of
|
|
68
89
|
* milliseconds.
|
|
69
90
|
* @param format - The template string containing tokens.
|
|
70
|
-
* @param options - How to handle the remainder
|
|
91
|
+
* @param options - How to handle the remainder, the month/year approximation and
|
|
92
|
+
* the frame rate.
|
|
71
93
|
* @returns Formatted duration string. An unknown token is left untouched, braces
|
|
72
94
|
* included.
|
|
73
95
|
* @see {@link getDurationParts} to reach the same parts as plain values.
|
|
96
|
+
* @see {@link formatDate} — the same template grammar, for a point in time rather
|
|
97
|
+
* than a length of it.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* This function and `formatDate` share one grammar: `{{…}}` delimiters, everything
|
|
101
|
+
* outside them literal, the bare token being the plain number and the doubled one
|
|
102
|
+
* that same number padded to two digits, `ms` padded to three, and an unknown token
|
|
103
|
+
* left as written. No token means two different kinds of thing across the two.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* **The template decides the breakdown, so nothing is ever lost for not having been
|
|
107
|
+
* asked for.** `'{{w}}w {{h}}h'` counts hours up to 167 because the week is there and
|
|
108
|
+
* the day is not; `'{{mm}}:{{ss}}'` on an hour-long video reads `62:05` rather than
|
|
109
|
+
* dropping the hour. The single-unit case follows from the same rule: `'{{s}}s'`
|
|
110
|
+
* carries the whole duration.
|
|
74
111
|
*
|
|
75
112
|
* @remarks
|
|
76
113
|
* A negative duration yields negative parts, so every token renders a signed
|
|
@@ -84,11 +121,11 @@ function unitsInFormat(format) {
|
|
|
84
121
|
* // => "1:02:05"
|
|
85
122
|
*/
|
|
86
123
|
export function formatDuration(duration, format, options = {}) {
|
|
87
|
-
const { floorSmallestUnit = true, useApproximateMonthAndYear = true } = options;
|
|
124
|
+
const { floorSmallestUnit = true, useApproximateMonthAndYear = true, fps = 25 } = options;
|
|
88
125
|
const parts = getDurationParts(duration, {
|
|
89
126
|
units: unitsInFormat(format),
|
|
90
127
|
floorSmallestUnit,
|
|
91
128
|
useApproximateMonthAndYear
|
|
92
129
|
});
|
|
93
|
-
return format.replace(tokenRegexp, (_match, token) => renderers[token](parts));
|
|
130
|
+
return format.replace(tokenRegexp, (_match, token) => renderers[token](parts, fps));
|
|
94
131
|
}
|
|
@@ -53,6 +53,33 @@ describe('formatDuration', () => {
|
|
|
53
53
|
expect(formatDuration(seconds(-90), '{{m}}:{{ss}}')).toBe('-1:-30');
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
describe('frames', () => {
|
|
57
|
+
it('counts the sub-second remainder in frames, at 25 fps by default', () => {
|
|
58
|
+
expect(formatDuration(3725240, '{{mm}}:{{ss}}.{{ff}}')).toBe('62:05.06');
|
|
59
|
+
expect(formatDuration(3725240, '{{mm}}:{{ss}}.{{f}}')).toBe('62:05.6');
|
|
60
|
+
});
|
|
61
|
+
it('counts in the frame rate it is given', () => {
|
|
62
|
+
expect(formatDuration(3725240, '{{ss}}.{{ff}}', { fps: 50 })).toBe('3725.12');
|
|
63
|
+
});
|
|
64
|
+
// A frame is reached once it has fully elapsed, so the 40th millisecond is
|
|
65
|
+
// where frame 1 starts at 25 fps — not the 39th.
|
|
66
|
+
it('truncates rather than rounds to the frame', () => {
|
|
67
|
+
expect(formatDuration(39, '{{s}}.{{ff}}')).toBe('0.00');
|
|
68
|
+
expect(formatDuration(40, '{{s}}.{{ff}}')).toBe('0.01');
|
|
69
|
+
});
|
|
70
|
+
// Frames are milliseconds in another base, so they pull the cascade down to
|
|
71
|
+
// milliseconds exactly as `{{ms}}` would — without needing `{{ms}}` written.
|
|
72
|
+
it('runs the cascade down to milliseconds without a ms token', () => {
|
|
73
|
+
expect(formatDuration(3725240, '{{ss}}')).toBe('3725');
|
|
74
|
+
expect(formatDuration(3725240, '{{ss}}.{{ff}}')).toBe('3725.06');
|
|
75
|
+
});
|
|
76
|
+
it('puts the whole duration on frames when it is the only token', () => {
|
|
77
|
+
expect(formatDuration(seconds(2), '{{f}}')).toBe('50');
|
|
78
|
+
});
|
|
79
|
+
it('signs a frame count like every other part', () => {
|
|
80
|
+
expect(formatDuration(-240, '{{s}}.{{f}}')).toBe('0.-6');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
56
83
|
});
|
|
57
84
|
describe('getDurationParts', () => {
|
|
58
85
|
describe('breakdown', () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export type DurationUnit = 'ms' | 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'Y';
|
|
6
6
|
/** Every token `formatDuration` substitutes in a template. */
|
|
7
|
-
export type DurationToken = 'Y' | 'YY' | 'M' | 'MM' | 'w' | 'ww' | 'd' | 'dd' | 'h' | 'hh' | 'm' | 'mm' | 's' | 'ss' | 'ms';
|
|
7
|
+
export type DurationToken = 'Y' | 'YY' | 'M' | 'MM' | 'w' | 'ww' | 'd' | 'dd' | 'h' | 'hh' | 'm' | 'mm' | 's' | 'ss' | 'ms' | 'f' | 'ff';
|
|
8
8
|
/** Options for `getDurationParts`. */
|
|
9
9
|
export type GetDurationPartsOptions = {
|
|
10
10
|
/**
|
|
@@ -25,6 +25,16 @@ export type GetDurationPartsOptions = {
|
|
|
25
25
|
* 365.2425 days and a month of exactly a twelfth of it.
|
|
26
26
|
*/
|
|
27
27
|
useApproximateMonthAndYear?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Frame rate the `f` and `ff` tokens count in. Defaults to `25`.
|
|
30
|
+
*
|
|
31
|
+
* Frames are a *rendering* of the millisecond part, not a unit of their own —
|
|
32
|
+
* they never appear in {@link DurationParts}, and a template asking for them
|
|
33
|
+
* asks for milliseconds under another base. So `'{{ss}}.{{ff}}'` reads the
|
|
34
|
+
* sub-second remainder, while `'{{f}}'` alone puts the whole duration on
|
|
35
|
+
* frames, exactly as `'{{ms}}'` alone puts it on milliseconds.
|
|
36
|
+
*/
|
|
37
|
+
fps?: number;
|
|
28
38
|
};
|
|
29
39
|
/**
|
|
30
40
|
* A duration's parts, keyed by unit token.
|
|
@@ -90,4 +100,14 @@ export type FormatDurationOptions = {
|
|
|
90
100
|
* 365.2425 days and a month of exactly a twelfth of it.
|
|
91
101
|
*/
|
|
92
102
|
useApproximateMonthAndYear?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Frame rate the `f` and `ff` tokens count in. Defaults to `25`.
|
|
105
|
+
*
|
|
106
|
+
* Frames are a *rendering* of the millisecond part, not a unit of their own —
|
|
107
|
+
* they never appear in {@link DurationParts}, and a template asking for them
|
|
108
|
+
* asks for milliseconds under another base. So `'{{ss}}.{{ff}}'` reads the
|
|
109
|
+
* sub-second remainder, while `'{{f}}'` alone puts the whole duration on
|
|
110
|
+
* frames, exactly as `'{{ms}}'` alone puts it on milliseconds.
|
|
111
|
+
*/
|
|
112
|
+
fps?: number;
|
|
93
113
|
};
|
package/agnostic/time/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * as dates from './dates/index.js'
|
|
2
2
|
export * as duration from './duration/index.js'
|
|
3
3
|
export * as timeout from './timeout/index.js'
|
|
4
|
-
export * as wait from './wait/index.js'
|
|
5
4
|
export * as transitions from './transitions/index.js'
|
|
5
|
+
export * as wait from './wait/index.js'
|
package/agnostic/time/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * as dates from './dates/index.js'
|
|
2
2
|
export * as duration from './duration/index.js'
|
|
3
3
|
export * as timeout from './timeout/index.js'
|
|
4
|
-
export * as wait from './wait/index.js'
|
|
5
4
|
export * as transitions from './transitions/index.js'
|
|
5
|
+
export * as wait from './wait/index.js'
|
|
@@ -66,6 +66,8 @@ type TrackData = {
|
|
|
66
66
|
* the component reacts, with the target and current rates.
|
|
67
67
|
* @property onFullscreenButtonClicked - Called when the fullscreen button is
|
|
68
68
|
* clicked, before the component reacts, with the fullscreen state as it was.
|
|
69
|
+
* @property onSubtitlesButtonClicked - Called when the subtitles button is clicked,
|
|
70
|
+
* before the component reacts, with the subtitles state as it was.
|
|
69
71
|
* @property onTimelineClicked - Called when the timeline is clicked, before the
|
|
70
72
|
* component reacts, with the target and current times (in seconds). The component
|
|
71
73
|
* seeks to the target right after, unless the time is controlled.
|
|
@@ -77,6 +79,8 @@ type TrackData = {
|
|
|
77
79
|
* @property onIsEndedChanged - Called after playback reached the end, and again
|
|
78
80
|
* once it left it — a seek back or a new play. Never on mount.
|
|
79
81
|
* @property onIsLoudChanged - Called once the mute state has changed (`true` = unmuted).
|
|
82
|
+
* @property onIsSubtitlesOnChanged - Called once the subtitles have been shown or
|
|
83
|
+
* hidden. Never on mount.
|
|
80
84
|
* @property onVolumeChanged - Called once the volume has changed (`0` to `1`).
|
|
81
85
|
* @property onPlaybackRateChanged - Called once the playback rate has changed.
|
|
82
86
|
* @property onCurrentTimeMsChanged - Called once the current time has changed, in
|
|
@@ -85,12 +89,24 @@ type TrackData = {
|
|
|
85
89
|
* fullscreen on its own — pressing Escape, typically — with the new state. This is
|
|
86
90
|
* the signal a parent needs to update its `fullscreen` prop, and it is distinct
|
|
87
91
|
* from `onIsFullscreenChanged`, which merely echoes that prop back once changed.
|
|
88
|
-
* @property timeFormat - How the two clocks are written, as a {@link
|
|
92
|
+
* @property timeFormat - How the two clocks are written, as a {@link formatDuration}
|
|
89
93
|
* pattern. Tokens go in `{{…}}` and the rest is literal, so `'{{m}} min {{ss}}'` is a
|
|
90
94
|
* valid answer. Defaults to `'{{mm}}:{{ss}}:{{ms}}'` — milliseconds included, which is
|
|
91
95
|
* what a cutting room wants and almost never what an article does: `'{{mm}}:{{ss}}'` is
|
|
92
|
-
* the usual one. Frames (`{{
|
|
96
|
+
* the usual one. Frames (`{{f}}`, `{{ff}}`) are derived at 25 fps, not configurable
|
|
93
97
|
* here.
|
|
98
|
+
*
|
|
99
|
+
* The pattern decides how the duration is split: the units it names are the units it
|
|
100
|
+
* gets, each taking its whole part and handing the rest down. So `'{{mm}}:{{ss}}'` on
|
|
101
|
+
* an hour-long video reads `62:05` rather than dropping the hour — nothing is ever
|
|
102
|
+
* lost for not having been asked for.
|
|
103
|
+
* @property subtitlesBtnContent - Glyph for the subtitles button, like the five other
|
|
104
|
+
* button contents. The button renders only when `subtitles` is set: with no cues there
|
|
105
|
+
* is nothing to toggle, and unlike `Disclaimer`'s missing toggler this withholds no
|
|
106
|
+
* behaviour — a stylesheet has nothing it could do with it either.
|
|
107
|
+
* @property subtitlesOn - Whether the subtitles show. Controlled: the component never
|
|
108
|
+
* changes it, it reports a click through `onSubtitlesButtonClicked` and leaves the
|
|
109
|
+
* decision to the parent. `Video` holds it for you.
|
|
94
110
|
* @property className - Additional CSS class for the root element.
|
|
95
111
|
* @property children - React content inserted into the <video> tag (fallback, etc).
|
|
96
112
|
*
|
|
@@ -105,7 +121,9 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
105
121
|
loudBtnContent?: React.ReactNode;
|
|
106
122
|
muteBtnContent?: React.ReactNode;
|
|
107
123
|
fullscreenBtnContent?: React.ReactNode;
|
|
124
|
+
subtitlesBtnContent?: React.ReactNode;
|
|
108
125
|
play?: boolean;
|
|
126
|
+
subtitlesOn?: boolean;
|
|
109
127
|
fullscreen?: boolean;
|
|
110
128
|
volume?: number;
|
|
111
129
|
mute?: boolean;
|
|
@@ -119,10 +137,12 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
119
137
|
onVolumeRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetVolume: number, currentVolume: number, video: HTMLVideoElement | null) => void;
|
|
120
138
|
onRateRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, currentRate: number, video: HTMLVideoElement | null) => void;
|
|
121
139
|
onFullscreenButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
|
|
140
|
+
onSubtitlesButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isSubtitlesOn: boolean, video: HTMLVideoElement | null) => void;
|
|
122
141
|
onTimelineClicked?: (e: React.MouseEvent<HTMLDivElement>, targetTime: number, currentTime: number, video: HTMLVideoElement | null) => void;
|
|
123
142
|
onIsPlayingChanged?: (isPlaying: boolean) => void;
|
|
124
143
|
onIsFullscreenChanged?: (isFullscreen: boolean) => void;
|
|
125
144
|
onIsLoudChanged?: (isLoud: boolean) => void;
|
|
145
|
+
onIsSubtitlesOnChanged?: (isSubtitlesOn: boolean) => void;
|
|
126
146
|
onIsEndedChanged?: (isEnded: boolean) => void;
|
|
127
147
|
onVolumeChanged?: (volume: number) => void;
|
|
128
148
|
onPlaybackRateChanged?: (playbackRate: number) => void;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
|
+
import { formatDuration } from '../../agnostic/time/duration/format-duration/index.js';
|
|
4
5
|
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
5
6
|
import cssModule from './styles.module.css';
|
|
6
7
|
import { video as publicClassName } from '../public-classnames.js';
|
|
7
8
|
import { Subtitles } from '../Subtitles/index.js';
|
|
8
|
-
import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume,
|
|
9
|
+
import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause, forcePlay, forcePlaybackRate, forceVolume, getTimelineClickProgress, msToSeconds, secondsToMs } from './utils.js';
|
|
9
10
|
/**
|
|
10
11
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
11
12
|
* playback controls, volume, playback rate, a timeline, optional subtitles
|
|
@@ -49,11 +50,14 @@ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause,
|
|
|
49
50
|
* @returns A `<figure>` element containing the video, its controls, optional
|
|
50
51
|
* subtitles.
|
|
51
52
|
*/
|
|
52
|
-
export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pauseBtnContent, loudBtnContent, muteBtnContent, fullscreenBtnContent, play, fullscreen,
|
|
53
|
+
export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pauseBtnContent, loudBtnContent, muteBtnContent, fullscreenBtnContent, subtitlesBtnContent, play, fullscreen,
|
|
54
|
+
// Les sous-titres sont montrés tant que personne n'a dit le contraire : un article qui
|
|
55
|
+
// en fournit veut qu'on les lise.
|
|
56
|
+
subtitlesOn = true, mute, muted, volume = 1, playbackRate = 1, currentTimeMs: givenCurrentTimeMs,
|
|
53
57
|
// Milliseconds by default, which is what this component has always rendered — a
|
|
54
58
|
// default that changes under a consumer is a change nobody asked for. An article
|
|
55
59
|
// usually wants `'{{mm}}:{{ss}}'` and says so.
|
|
56
|
-
timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onTimelineClicked, onIsPlayingChanged, onIsFullscreenChanged, onIsLoudChanged, onIsEndedChanged, onVolumeChanged, onPlaybackRateChanged, onCurrentTimeMsChanged, onFullscreenChange, children, className, ...intrinsicVideoAttributes }) => {
|
|
60
|
+
timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onSubtitlesButtonClicked, onTimelineClicked, onIsPlayingChanged, onIsFullscreenChanged, onIsLoudChanged, onIsSubtitlesOnChanged, onIsEndedChanged, onVolumeChanged, onPlaybackRateChanged, onCurrentTimeMsChanged, onFullscreenChange, children, className, ...intrinsicVideoAttributes }) => {
|
|
57
61
|
const videoRef = useRef(null);
|
|
58
62
|
const [totalTime, setTotalTime] = useState(0);
|
|
59
63
|
const totalTimeMs = useMemo(() => secondsToMs(totalTime), [totalTime]);
|
|
@@ -130,6 +134,9 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
130
134
|
const handleFullscreenButtonClick = useCallback((e) => {
|
|
131
135
|
onFullscreenButtonClicked?.(e, isFullscreen, videoRef.current);
|
|
132
136
|
}, [onFullscreenButtonClicked, isFullscreen]);
|
|
137
|
+
const handleSubtitlesButtonClick = useCallback((e) => {
|
|
138
|
+
onSubtitlesButtonClicked?.(e, subtitlesOn, videoRef.current);
|
|
139
|
+
}, [onSubtitlesButtonClicked, subtitlesOn]);
|
|
133
140
|
const handleVolumeRangeChange = useCallback((e) => {
|
|
134
141
|
const targetVolume = Number(e.currentTarget.value) / 100;
|
|
135
142
|
onVolumeRangeChanged?.(e, targetVolume, volume, videoRef.current);
|
|
@@ -158,7 +165,12 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
158
165
|
'fullscreen-on': isFullscreen,
|
|
159
166
|
'fullscreen-off': !isFullscreen,
|
|
160
167
|
'loud': isLoud,
|
|
161
|
-
'muted': !isLoud
|
|
168
|
+
'muted': !isLoud,
|
|
169
|
+
// Rien des deux quand aucun sous-titre n'est fourni : l'absence n'est pas un état
|
|
170
|
+
// caché, c'est l'absence d'un état. Une feuille qui veut viser ce cas le reconnaît à
|
|
171
|
+
// ce qu'aucun des deux modifieurs n'est là.
|
|
172
|
+
'subtitles-on': subtitles !== undefined && subtitlesOn,
|
|
173
|
+
'subtitles-off': subtitles !== undefined && !subtitlesOn
|
|
162
174
|
}), className);
|
|
163
175
|
// Guarded: the duration is unknown until the metadata lands, and an unguarded
|
|
164
176
|
// division would expose the string 'NaN' on every render until then.
|
|
@@ -171,6 +183,8 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
171
183
|
'data-fullscreen-off': !isFullscreen ? '' : undefined,
|
|
172
184
|
'data-loud': isLoud ? '' : undefined,
|
|
173
185
|
'data-muted': !isLoud ? '' : undefined,
|
|
186
|
+
'data-subtitles-on': subtitles !== undefined && subtitlesOn ? '' : undefined,
|
|
187
|
+
'data-subtitles-off': subtitles !== undefined && !subtitlesOn ? '' : undefined,
|
|
174
188
|
'data-volume': volume.toFixed(8),
|
|
175
189
|
'data-volume-percent': volumePercent,
|
|
176
190
|
'data-playback-rate': playbackRate,
|
|
@@ -227,6 +241,7 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
227
241
|
const muteBtnClss = c('mute-btn');
|
|
228
242
|
const volumePcntClss = c('volume-percent');
|
|
229
243
|
const fullscreenBtnClss = c('fullscreen-btn');
|
|
244
|
+
const subtitlesBtnClss = c('subtitles-btn');
|
|
230
245
|
const volumeRangeClss = c('volume-range');
|
|
231
246
|
const playbackRateRangeClss = c('playback-rate-range');
|
|
232
247
|
const playbackRateClss = c('playback-rate');
|
|
@@ -299,6 +314,7 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
299
314
|
useChangeDispatch(isElementPlaying, onIsPlayingChanged);
|
|
300
315
|
useChangeDispatch(isFullscreen, onIsFullscreenChanged);
|
|
301
316
|
useChangeDispatch(isLoud, onIsLoudChanged);
|
|
317
|
+
useChangeDispatch(subtitlesOn, onIsSubtitlesOnChanged);
|
|
302
318
|
useChangeDispatch(isEnded, onIsEndedChanged);
|
|
303
319
|
useChangeDispatch(volume, onVolumeChanged);
|
|
304
320
|
useChangeDispatch(playbackRate, onPlaybackRateChanged);
|
|
@@ -306,5 +322,5 @@ timeFormat = '{{mm}}:{{ss}}:{{ms}}', onPlayButtonClicked, onPauseButtonClicked,
|
|
|
306
322
|
? _jsx("source", { src: source }, index)
|
|
307
323
|
: _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => typeof track === 'string'
|
|
308
324
|
? _jsx("track", { src: track }, index)
|
|
309
|
-
: _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type: 'range', className: volumeRangeClss, value: volumePercent, onChange: handleVolumeRangeChange, min: 0, max: 100, step: 1 }), _jsx("span", { className: volumePcntClss, children: Math.round(volumePercent) }), _jsx("button", { className: fullscreenBtnClss, onClick: handleFullscreenButtonClick, children: fullscreenBtnContent }), _jsx("input", { type: 'range', className: playbackRateRangeClss, value: playbackRate, onChange: handleRateRangeChange, min: 0.25, max: 4, step: 0.25 }), _jsx("span", { className: playbackRateClss, children: playbackRate })] }), _jsxs("div", { className: timeControlsClss, children: [_jsx("span", { className: currentTimeClss, children:
|
|
325
|
+
: _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type: 'range', className: volumeRangeClss, value: volumePercent, onChange: handleVolumeRangeChange, min: 0, max: 100, step: 1 }), _jsx("span", { className: volumePcntClss, children: Math.round(volumePercent) }), _jsx("button", { className: fullscreenBtnClss, onClick: handleFullscreenButtonClick, children: fullscreenBtnContent }), subtitles !== undefined && _jsx("button", { className: subtitlesBtnClss, onClick: handleSubtitlesButtonClick, children: subtitlesBtnContent }), _jsx("input", { type: 'range', className: playbackRateRangeClss, value: playbackRate, onChange: handleRateRangeChange, min: 0.25, max: 4, step: 0.25 }), _jsx("span", { className: playbackRateClss, children: playbackRate })] }), _jsxs("div", { className: timeControlsClss, children: [_jsx("span", { className: currentTimeClss, children: formatDuration(currentTimeMs, timeFormat) }), _jsx("span", { className: totalTimeClss, children: formatDuration(totalTimeMs, timeFormat) }), _jsx("div", { className: timelineClss, onClick: handleTimelineClick })] }), subtitles !== undefined && _jsx(Subtitles, { ...subtitles, timecodeMs: currentTimeMs, isEnded: isEnded })] });
|
|
310
326
|
};
|
|
@@ -31,11 +31,16 @@ import { type Props as ControlledProps } from './index.controlled.js';
|
|
|
31
31
|
* `autoPlay`, `autoPlayWhenVisible` and the play button have no effect for as
|
|
32
32
|
* long as this prop is provided.
|
|
33
33
|
* @property wrapperClassName - Optional additional class name(s) applied to the root wrapper element.
|
|
34
|
+
* @property defaultSubtitlesOn - Whether the subtitles start shown. `true` by default —
|
|
35
|
+
* an article that supplies cues means them to be read. The reader's button takes it from
|
|
36
|
+
* there, so this is a starting point and not a setting: `default…` and never `initial…`,
|
|
37
|
+
* as everywhere else here.
|
|
34
38
|
* @property className - Optional additional class name(s) applied to the root element.
|
|
35
39
|
* @property children - React children rendered inside the `<video>` element itself
|
|
36
40
|
* (e.g. fallback content).
|
|
37
41
|
*/
|
|
38
|
-
export type Props = WithViewportObservation<Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate'>> & {
|
|
42
|
+
export type Props = WithViewportObservation<Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate' | 'subtitlesOn'>> & {
|
|
43
|
+
defaultSubtitlesOn?: boolean;
|
|
39
44
|
autoPlayWhenVisible?: boolean;
|
|
40
45
|
autoPlayOnceVisible?: boolean;
|
|
41
46
|
autoPauseWhenHidden?: boolean;
|
|
@@ -73,6 +78,13 @@ export type Props = WithViewportObservation<Omit<ControlledProps, 'play' | 'full
|
|
|
73
78
|
* automatic play the component still owed. Setting both flavours of the same
|
|
74
79
|
* behaviour is the same as setting the `…When…` one alone.
|
|
75
80
|
*
|
|
81
|
+
* **`loop` reaches the element untouched**, like every other native media attribute
|
|
82
|
+
* this component does not drive itself. It was once destructured out of the props and
|
|
83
|
+
* never forwarded, which silently dropped it — so if a native attribute ever stops
|
|
84
|
+
* arriving, look at the destructuring here before looking at the caller. One
|
|
85
|
+
* consequence worth knowing: a looping element never fires `ended`, so the `--ended`
|
|
86
|
+
* modifier and the `isEnded` it feeds to `Subtitles` simply never come.
|
|
87
|
+
*
|
|
76
88
|
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing an
|
|
77
89
|
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected. The
|
|
78
90
|
* refusal is caught rather than ignored — the element is read back once the attempt
|
|
@@ -34,6 +34,13 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
34
34
|
* automatic play the component still owed. Setting both flavours of the same
|
|
35
35
|
* behaviour is the same as setting the `…When…` one alone.
|
|
36
36
|
*
|
|
37
|
+
* **`loop` reaches the element untouched**, like every other native media attribute
|
|
38
|
+
* this component does not drive itself. It was once destructured out of the props and
|
|
39
|
+
* never forwarded, which silently dropped it — so if a native attribute ever stops
|
|
40
|
+
* arriving, look at the destructuring here before looking at the caller. One
|
|
41
|
+
* consequence worth knowing: a looping element never fires `ended`, so the `--ended`
|
|
42
|
+
* modifier and the `isEnded` it feeds to `Subtitles` simply never come.
|
|
43
|
+
*
|
|
37
44
|
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing an
|
|
38
45
|
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected. The
|
|
39
46
|
* refusal is caught rather than ignored — the element is read back once the attempt
|
|
@@ -41,13 +48,14 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
41
48
|
* The media still won't play, though: autoplay muted, and leave unmuting to the
|
|
42
49
|
* reader.
|
|
43
50
|
*/
|
|
44
|
-
export const Video = ({
|
|
51
|
+
export const Video = ({ defaultSubtitlesOn = true, autoPlayWhenVisible, autoPlayOnceVisible, autoPauseWhenHidden, autoPauseOnceHidden, autoMuteWhenHidden, autoMuteOnceHidden, autoLoudWhenVisible, autoLoudOnceVisible, threshold, root, rootMargin, onVisibilityChanged, wrapperClassName, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onSubtitlesButtonClicked, ...controlledProps }) => {
|
|
45
52
|
// State & refs
|
|
46
53
|
const [play, setPlay] = useState(false);
|
|
47
54
|
const [volume, setVolume] = useState(1);
|
|
48
55
|
const [mute, setMute] = useState(false);
|
|
49
56
|
const [playbackRate, setPlaybackRate] = useState(1);
|
|
50
57
|
const [fullscreen, setFullscreen] = useState(false);
|
|
58
|
+
const [subtitlesOn, setSubtitlesOn] = useState(defaultSubtitlesOn);
|
|
51
59
|
// One flag per `…Once…` behaviour, and each is armed by that behaviour's own
|
|
52
60
|
// automatic trigger. A single shared flag conflated four questions, and being set
|
|
53
61
|
// on any `play` event — a user click included — meant the first press cancelled
|
|
@@ -143,6 +151,14 @@ export const Video = ({ loop, autoPlayWhenVisible, autoPlayOnceVisible, autoPaus
|
|
|
143
151
|
onFullscreenButtonClicked?.(e, isFullscreen, video);
|
|
144
152
|
setFullscreen(!isFullscreen);
|
|
145
153
|
}, [onFullscreenButtonClicked]);
|
|
154
|
+
// Un seul bouton pour les deux sens, contrairement à lecture/pause et son/muet qui en
|
|
155
|
+
// ont un chacun : ceux-là existent en paire dans le DOM et la feuille en montre un,
|
|
156
|
+
// parce qu'ils portent deux glyphes opposés. Les sous-titres n'en portent qu'un, dont
|
|
157
|
+
// seul l'habillage change — inutile d'en rendre deux pour n'en montrer jamais qu'un.
|
|
158
|
+
const handleSubtitlesButtonClick = useCallback((e, isSubtitlesOn, video) => {
|
|
159
|
+
onSubtitlesButtonClicked?.(e, isSubtitlesOn, video);
|
|
160
|
+
setSubtitlesOn(!isSubtitlesOn);
|
|
161
|
+
}, [onSubtitlesButtonClicked]);
|
|
146
162
|
// Intersection Observer
|
|
147
163
|
const onIntersected = useCallback(({ ioEntry }) => {
|
|
148
164
|
if (ioEntry === undefined)
|
|
@@ -188,7 +204,7 @@ export const Video = ({ loop, autoPlayWhenVisible, autoPlayOnceVisible, autoPaus
|
|
|
188
204
|
// Render
|
|
189
205
|
const c = clss(publicClassName, { cssModule });
|
|
190
206
|
const rootClss = mergeClassNames(c(), wrapperClassName);
|
|
191
|
-
const videoContent = _jsx(ControlledVideo, { ...controlledProps, play: play && !isTimeControlled, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onIsPlayingChanged: handleIsPlayingChanged, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleFullscreenChange, onPlayButtonClicked: handlePlayButtonClick, onPauseButtonClicked: handlePauseButtonClick, onLoudButtonClicked: handleLoudButtonClick, onMuteButtonClicked: handleMuteButtonClick, onVolumeRangeChanged: handleVolumeRangeChange, onRateRangeChanged: handleRateRangeChange, onFullscreenButtonClicked: handleFullscreenButtonClick });
|
|
207
|
+
const videoContent = _jsx(ControlledVideo, { ...controlledProps, play: play && !isTimeControlled, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, subtitlesOn: subtitlesOn, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onIsPlayingChanged: handleIsPlayingChanged, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleFullscreenChange, onPlayButtonClicked: handlePlayButtonClick, onPauseButtonClicked: handlePauseButtonClick, onLoudButtonClicked: handleLoudButtonClick, onMuteButtonClicked: handleMuteButtonClick, onVolumeRangeChanged: handleVolumeRangeChange, onRateRangeChanged: handleRateRangeChange, onFullscreenButtonClicked: handleFullscreenButtonClick, onSubtitlesButtonClicked: handleSubtitlesButtonClick });
|
|
192
208
|
return _jsx("div", { className: rootClss, children: needsObserve
|
|
193
209
|
? _jsx(IntersectionObserverComponent, { threshold: threshold, root: root, rootMargin: rootMargin, onIntersected: onIntersected, children: videoContent })
|
|
194
210
|
: videoContent });
|
|
@@ -20,37 +20,6 @@ export declare const forceFullscreen: (video: HTMLVideoElement | null) => Promis
|
|
|
20
20
|
export declare const forceExitFullscreen: (video: HTMLVideoElement | null) => Promise<boolean>;
|
|
21
21
|
export declare function secondsToMs(seconds: number): number;
|
|
22
22
|
export declare function msToSeconds(ms: number): number;
|
|
23
|
-
/** The fields {@link formatTime} knows how to render. */
|
|
24
|
-
export type TimeToken = 'hh' | 'mm' | 'ss' | 'ms' | 'frame' | 'h' | 'm' | 's' | 'f';
|
|
25
|
-
/**
|
|
26
|
-
* Renders a duration into a caller-defined pattern.
|
|
27
|
-
*
|
|
28
|
-
* Tokens are wrapped in `{{…}}` and everything outside them is literal, so a pattern
|
|
29
|
-
* may carry words: `'{{m}} min {{ss}}'` renders as `'1 min 05'`.
|
|
30
|
-
*
|
|
31
|
-
* - `{{hh}}`, `{{mm}}`, `{{ss}}` — hours, minutes and seconds, zero-padded to two digits.
|
|
32
|
-
* - `{{ms}}` — the remaining milliseconds, zero-padded to three digits.
|
|
33
|
-
* - `{{frame}}` — those milliseconds expressed in frames, zero-padded to two digits.
|
|
34
|
-
* - `{{h}}`, `{{m}}`, `{{s}}`, `{{f}}` — the same four values, unpadded.
|
|
35
|
-
*
|
|
36
|
-
* @param ms - The duration, in milliseconds.
|
|
37
|
-
* @param format - The pattern to fill, e.g. `'{{mm}}:{{ss}}'`.
|
|
38
|
-
* @param fps - Frame rate used to derive the `frame` and `f` tokens. Defaults to `25`.
|
|
39
|
-
* @returns The formatted duration. An unknown token is left as written, braces
|
|
40
|
-
* included, rather than silently blanked — a typo has to be visible.
|
|
41
|
-
*
|
|
42
|
-
* @remarks
|
|
43
|
-
* Every field is a remainder of the one above it, never a total: at one hour and a half,
|
|
44
|
-
* `{{mm}}` is `30`, not `90`. A pattern that omits `{{hh}}` therefore loses the hours
|
|
45
|
-
* rather than folding them into the minutes.
|
|
46
|
-
*
|
|
47
|
-
* **The delimiters are what make the pattern free-form.** Until they existed, `h`, `m`,
|
|
48
|
-
* `s` and `f` were tokens on their own and every occurrence of those letters was
|
|
49
|
-
* substituted wherever it sat — `'mm min ss'` rendered as `'01 1in 01'`, so a pattern
|
|
50
|
-
* could only ever separate its fields with punctuation. That is why the format could not
|
|
51
|
-
* be exposed as a prop: doing so would have published the trap.
|
|
52
|
-
*/
|
|
53
|
-
export declare function formatTime(ms: number, format: string, fps?: number): string;
|
|
54
23
|
/**
|
|
55
24
|
* Where along the timeline a click landed.
|
|
56
25
|
*
|
|
@@ -107,64 +107,6 @@ export function secondsToMs(seconds) {
|
|
|
107
107
|
export function msToSeconds(ms) {
|
|
108
108
|
return ms / 1000;
|
|
109
109
|
}
|
|
110
|
-
// Alternation is ordered and the first branch that fits wins, so the longest tokens
|
|
111
|
-
// come first — the same regexp shape, and the same reason, as
|
|
112
|
-
// `agnostic/time/dates/format-date`.
|
|
113
|
-
const timeTokenRegexp = /\{\{(frame|hh|mm|ss|ms|h|m|s|f)\}\}/gv;
|
|
114
|
-
/**
|
|
115
|
-
* Renders a duration into a caller-defined pattern.
|
|
116
|
-
*
|
|
117
|
-
* Tokens are wrapped in `{{…}}` and everything outside them is literal, so a pattern
|
|
118
|
-
* may carry words: `'{{m}} min {{ss}}'` renders as `'1 min 05'`.
|
|
119
|
-
*
|
|
120
|
-
* - `{{hh}}`, `{{mm}}`, `{{ss}}` — hours, minutes and seconds, zero-padded to two digits.
|
|
121
|
-
* - `{{ms}}` — the remaining milliseconds, zero-padded to three digits.
|
|
122
|
-
* - `{{frame}}` — those milliseconds expressed in frames, zero-padded to two digits.
|
|
123
|
-
* - `{{h}}`, `{{m}}`, `{{s}}`, `{{f}}` — the same four values, unpadded.
|
|
124
|
-
*
|
|
125
|
-
* @param ms - The duration, in milliseconds.
|
|
126
|
-
* @param format - The pattern to fill, e.g. `'{{mm}}:{{ss}}'`.
|
|
127
|
-
* @param fps - Frame rate used to derive the `frame` and `f` tokens. Defaults to `25`.
|
|
128
|
-
* @returns The formatted duration. An unknown token is left as written, braces
|
|
129
|
-
* included, rather than silently blanked — a typo has to be visible.
|
|
130
|
-
*
|
|
131
|
-
* @remarks
|
|
132
|
-
* Every field is a remainder of the one above it, never a total: at one hour and a half,
|
|
133
|
-
* `{{mm}}` is `30`, not `90`. A pattern that omits `{{hh}}` therefore loses the hours
|
|
134
|
-
* rather than folding them into the minutes.
|
|
135
|
-
*
|
|
136
|
-
* **The delimiters are what make the pattern free-form.** Until they existed, `h`, `m`,
|
|
137
|
-
* `s` and `f` were tokens on their own and every occurrence of those letters was
|
|
138
|
-
* substituted wherever it sat — `'mm min ss'` rendered as `'01 1in 01'`, so a pattern
|
|
139
|
-
* could only ever separate its fields with punctuation. That is why the format could not
|
|
140
|
-
* be exposed as a prop: doing so would have published the trap.
|
|
141
|
-
*/
|
|
142
|
-
export function formatTime(ms, format, fps = 25) {
|
|
143
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
144
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
145
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
146
|
-
const seconds = Math.floor(totalSeconds % 60);
|
|
147
|
-
const frames = Math.floor(((ms % 1000) / 1000) * fps);
|
|
148
|
-
const msRest = Math.floor(ms % 1000);
|
|
149
|
-
// Typed by {@link TimeToken} so the table and the regexp above cannot fall out of step,
|
|
150
|
-
// then read through a widened view: the capture is a `string` as far as the compiler
|
|
151
|
-
// knows, and widening is what lets it be looked up without asserting it back.
|
|
152
|
-
const tokens = {
|
|
153
|
-
hh: String(hours).padStart(2, '0'),
|
|
154
|
-
mm: String(minutes).padStart(2, '0'),
|
|
155
|
-
ss: String(seconds).padStart(2, '0'),
|
|
156
|
-
frame: String(frames).padStart(2, '0'),
|
|
157
|
-
ms: String(msRest).padStart(3, '0'),
|
|
158
|
-
h: String(hours),
|
|
159
|
-
m: String(minutes),
|
|
160
|
-
s: String(seconds),
|
|
161
|
-
f: String(frames)
|
|
162
|
-
};
|
|
163
|
-
const byName = tokens;
|
|
164
|
-
// One pass, so a rendered value can never be re-read as a token by a later pass — the
|
|
165
|
-
// reason the old implementation had to sort its keys by length.
|
|
166
|
-
return format.replace(timeTokenRegexp, (whole, token) => byName[token] ?? whole);
|
|
167
|
-
}
|
|
168
110
|
/**
|
|
169
111
|
* Where along the timeline a click landed.
|
|
170
112
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as copy from './copy/index.js'
|
|
2
2
|
export * as download from './download/index.js'
|
|
3
3
|
export * as exists from './exists/index.js'
|
|
4
|
-
export * as remove from './remove/index.js'
|
|
5
4
|
export * as move from './move/index.js'
|
|
6
5
|
export * as stat from './stat/index.js'
|
|
6
|
+
export * as remove from './remove/index.js'
|
|
7
7
|
export * as upload from './upload/index.js'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as copy from './copy/index.js'
|
|
2
2
|
export * as download from './download/index.js'
|
|
3
3
|
export * as exists from './exists/index.js'
|
|
4
|
-
export * as remove from './remove/index.js'
|
|
5
4
|
export * as move from './move/index.js'
|
|
6
5
|
export * as stat from './stat/index.js'
|
|
6
|
+
export * as remove from './remove/index.js'
|
|
7
7
|
export * as upload from './upload/index.js'
|