@applicaster/quick-brick-player 16.0.0-rc.9 → 16.0.0-rc.90
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/package.json +6 -6
- package/src/Player/AudioLayer/AudioDescription.tsx +46 -11
- package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +38 -48
- package/src/Player/AudioLayer/Layout/Controls/__tests__/utils.test.ts +87 -0
- package/src/Player/AudioLayer/Layout/Controls/index.tsx +34 -14
- package/src/Player/AudioLayer/Layout/Controls/utils.ts +40 -0
- package/src/Player/AudioLayer/Layout/DockedControls/index.tsx +1 -13
- package/src/Player/AudioLayer/Layout/MobileLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TabletLandscapeLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TabletPortraitLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TransportInfo/__tests__/index.test.tsx +257 -0
- package/src/Player/AudioLayer/Layout/TransportInfo/index.tsx +69 -8
- package/src/Player/AudioLayer/PlaylistNavigationController.ts +381 -0
- package/src/Player/AudioLayer/SleepTimerManager.ts +5 -1
- package/src/Player/AudioLayer/SpeedManager.ts +4 -0
- package/src/Player/AudioLayer/__tests__/AudioDescription.test.tsx +147 -0
- package/src/Player/AudioLayer/__tests__/AudioPlayerWrapper.test.tsx +130 -0
- package/src/Player/AudioLayer/__tests__/PlaylistNavigationController.test.ts +529 -0
- package/src/Player/AudioLayer/__tests__/SleepTimerManager.test.ts +78 -0
- package/src/Player/AudioLayer/__tests__/SpeedManager.test.ts +57 -0
- package/src/Player/AudioLayer/__tests__/playerActions.test.ts +369 -0
- package/src/Player/AudioLayer/__tests__/usePlaylistNavigation.test.ts +387 -0
- package/src/Player/AudioLayer/playerActions.ts +294 -0
- package/src/Player/AudioLayer/usePlaylistNavigation.ts +76 -0
- package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
- package/src/Player/PlayerModal/VideoPlayerModal.tsx +18 -1
- package/src/Player/PlayerModal/__tests__/VideoPlayerModal.test.tsx +160 -0
- package/src/Player/__tests__/getNativeProps.test.ts +115 -0
- package/src/Player/__tests__/onTracksChanged.test.ts +192 -0
- package/src/Player/__tests__/toNativeMetadata.test.ts +77 -0
- package/src/Player/index.tsx +147 -40
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +0 -66
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +0 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { toNativeMetadata } from "../index";
|
|
2
|
+
|
|
3
|
+
// `toNativeMetadata` used to be `resolveNativeTitleSubtitle`, which resolved
|
|
4
|
+
// Title/Subtitle from `entry` + `pluginConfiguration` itself (the dot-path /
|
|
5
|
+
// custom-key / fallback logic). That resolution now happens once in
|
|
6
|
+
// `Player.getContent()` (via `resolvePlayerTitleSubtitle`, already covered by
|
|
7
|
+
// `playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts` and
|
|
8
|
+
// `playerUtils/__tests__/contentDataKeys.test.ts` - custom-key resolution,
|
|
9
|
+
// fallback-when-unresolvable, and entry-non-mutation are all still asserted
|
|
10
|
+
// there). `toNativeMetadata` only adapts the already-resolved
|
|
11
|
+
// `player.getContent()` value into the `metadata` object the native bridge
|
|
12
|
+
// receives, so this file only covers that adaptation: the shape coercion, and
|
|
13
|
+
// the numeric coercion the native bridge specifically needs (it cannot render
|
|
14
|
+
// a number the way `<Text>` can).
|
|
15
|
+
describe("toNativeMetadata", () => {
|
|
16
|
+
it("returns undefined for both fields when title/summary are null (what the player publishes when there is nothing to show)", () => {
|
|
17
|
+
expect(toNativeMetadata({ title: null, summary: null })).toEqual({
|
|
18
|
+
title: undefined,
|
|
19
|
+
subtitle: undefined,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns undefined for both fields when content itself is undefined", () => {
|
|
24
|
+
expect(toNativeMetadata(undefined)).toEqual({
|
|
25
|
+
title: undefined,
|
|
26
|
+
subtitle: undefined,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("passes through string title/summary as-is", () => {
|
|
31
|
+
expect(
|
|
32
|
+
toNativeMetadata({
|
|
33
|
+
title: "Resolved Title",
|
|
34
|
+
summary: "Resolved Summary",
|
|
35
|
+
})
|
|
36
|
+
).toEqual({
|
|
37
|
+
title: "Resolved Title",
|
|
38
|
+
subtitle: "Resolved Summary",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("coerces a numeric title/summary to a string for the native bridge", () => {
|
|
43
|
+
expect(toNativeMetadata({ title: 0, summary: 42 })).toEqual({
|
|
44
|
+
title: "0",
|
|
45
|
+
subtitle: "42",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("handles title and summary independently (one present, one absent)", () => {
|
|
50
|
+
expect(toNativeMetadata({ title: "Only Title", summary: null })).toEqual({
|
|
51
|
+
title: "Only Title",
|
|
52
|
+
subtitle: undefined,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("passes the cover image url through", () => {
|
|
57
|
+
expect(
|
|
58
|
+
toNativeMetadata({
|
|
59
|
+
title: "A Title",
|
|
60
|
+
summary: "A Summary",
|
|
61
|
+
imageUrl: "https://img.test/base.jpg",
|
|
62
|
+
})
|
|
63
|
+
).toEqual({
|
|
64
|
+
title: "A Title",
|
|
65
|
+
subtitle: "A Summary",
|
|
66
|
+
imageUrl: "https://img.test/base.jpg",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("sends undefined rather than an empty string when there is no image", () => {
|
|
71
|
+
// Native treats an empty URI as a load attempt; `undefined` means "none".
|
|
72
|
+
expect(
|
|
73
|
+
toNativeMetadata({ title: "A Title", summary: null, imageUrl: null })
|
|
74
|
+
.imageUrl
|
|
75
|
+
).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/Player/index.tsx
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
import { PlayNextOverlay } from "./PlayNextOverlay";
|
|
32
32
|
import { PlayerImageBackground } from "@applicaster/zapp-react-native-ui-components/Components/PlayerImageBackground";
|
|
33
33
|
|
|
34
|
-
import { GENERAL_EVENT } from "@applicaster/zapp-react-native-utils/analyticsUtils/events";
|
|
35
34
|
import { parseLanguageTracks } from "@applicaster/zapp-react-native-utils/playerUtils/configurationUtils";
|
|
36
35
|
import { DEFAULT_COLORS, SEEK_TIME } from "../utils/const";
|
|
37
36
|
import { logger, getScreenAspectRatio } from "../utils";
|
|
@@ -41,7 +40,6 @@ import { getAllAccessibilityProps } from "@applicaster/zapp-react-native-utils/c
|
|
|
41
40
|
import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
|
|
42
41
|
import { getTargetScreenDataFromEntry } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData";
|
|
43
42
|
import { PlayerAnalyticsManager } from "@applicaster/zapp-react-native-utils/analyticsUtils/PlayerAnalyticsManager";
|
|
44
|
-
import { PlayerAnalyticsTrackerI } from "@applicaster/zapp-react-native-utils/analyticsUtils/playerAnalyticsTracker";
|
|
45
43
|
|
|
46
44
|
import { RNPlayerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/nativePlayerManager";
|
|
47
45
|
import {
|
|
@@ -54,7 +52,7 @@ import { TVPlayerView } from "./PlayerView/TVPlayerView";
|
|
|
54
52
|
import PlayerModal, { VideoPlayerModal } from "./PlayerModal";
|
|
55
53
|
import { DockedControls } from "./AudioLayer/Layout/DockedControls";
|
|
56
54
|
import { FlexImage } from "./AudioLayer/Layout/PlayerImage/FlexImage";
|
|
57
|
-
import { PlayerError } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/
|
|
55
|
+
import { PlayerError } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
58
56
|
|
|
59
57
|
const isTvOS = isTvOSPlatform();
|
|
60
58
|
|
|
@@ -71,6 +69,60 @@ const styles = StyleSheet.create({
|
|
|
71
69
|
flexImage: { height: "100%", aspectRatio: 1, flex: -1 },
|
|
72
70
|
});
|
|
73
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Coerces the resolver's `Option<string | number>` result into what the
|
|
74
|
+
* native bridge expects: a string, or `undefined` when there is nothing to
|
|
75
|
+
* send.
|
|
76
|
+
*/
|
|
77
|
+
const toNativeTextProp = (
|
|
78
|
+
value: Option<string | number>
|
|
79
|
+
): string | undefined =>
|
|
80
|
+
value === null || value === undefined ? undefined : String(value);
|
|
81
|
+
|
|
82
|
+
export type NativePlayerMetadata = {
|
|
83
|
+
title: string | undefined;
|
|
84
|
+
subtitle: string | undefined;
|
|
85
|
+
imageUrl: string | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Adapts the player's resolved Title/Subtitle into the `metadata` object the
|
|
90
|
+
* *native* view receives.
|
|
91
|
+
*
|
|
92
|
+
* The resolution itself (dot-path / key-set logic, entry fallback) does not
|
|
93
|
+
* happen here - the player manager resolves it and publishes it on its content
|
|
94
|
+
* channel (`player.getContent()`). This function only coerces that value's
|
|
95
|
+
* `Option<string | number>` shape into what the native bridge needs: a string,
|
|
96
|
+
* or `undefined` when there is nothing to send.
|
|
97
|
+
*
|
|
98
|
+
* Travels as one `metadata` object rather than two flat props so native has a
|
|
99
|
+
* single place to look for player-facing text, and so adding a field later
|
|
100
|
+
* does not widen the bridge's prop surface again.
|
|
101
|
+
*
|
|
102
|
+
* This intentionally does not touch `entry` - analytics and the downloads
|
|
103
|
+
* feature read `entry.title` / `entry.summary` directly and must keep
|
|
104
|
+
* seeing the original feed values (the downloads feature requires the
|
|
105
|
+
* original entry title). The resolved (possibly customer-configured) text
|
|
106
|
+
* travels in `metadata` instead.
|
|
107
|
+
*
|
|
108
|
+
* Exported as a plain function (rather than inlined as a class method) so
|
|
109
|
+
* it can be unit tested directly without rendering the ~37KB `VideoPlayer`
|
|
110
|
+
* class component.
|
|
111
|
+
*/
|
|
112
|
+
export const toNativeMetadata = (
|
|
113
|
+
content: Option<{
|
|
114
|
+
title: Option<string | number>;
|
|
115
|
+
summary: Option<string | number>;
|
|
116
|
+
imageUrl?: Option<string>;
|
|
117
|
+
}>
|
|
118
|
+
): NativePlayerMetadata => ({
|
|
119
|
+
title: toNativeTextProp(content?.title),
|
|
120
|
+
subtitle: toNativeTextProp(content?.summary),
|
|
121
|
+
// Already a string when present; coerced through the same helper so an
|
|
122
|
+
// empty or nullish value reaches native as `undefined` rather than "".
|
|
123
|
+
imageUrl: toNativeTextProp(content?.imageUrl),
|
|
124
|
+
});
|
|
125
|
+
|
|
74
126
|
type VideoPlayerProps = QuickBrickPlayer.PlayerProps & {
|
|
75
127
|
isCasting?: boolean;
|
|
76
128
|
playNextData?: PlayNextData;
|
|
@@ -96,6 +148,38 @@ type VideoPlayerProps = QuickBrickPlayer.PlayerProps & {
|
|
|
96
148
|
children?: React.ReactNode;
|
|
97
149
|
};
|
|
98
150
|
|
|
151
|
+
/**
|
|
152
|
+
* `textTrackId`/`audioTrackId` are both the value the native player *reports* as
|
|
153
|
+
* currently selected AND the *controlled prop* that drives native selection.
|
|
154
|
+
* Because of that dual role, reflecting a reported value straight back down turns
|
|
155
|
+
* a status update into a command.
|
|
156
|
+
*
|
|
157
|
+
* The native player can momentarily report "nothing selected" (index -1, which
|
|
158
|
+
* `parseLanguageTracks` maps to `undefined`) during normal fMP4/ABR segment
|
|
159
|
+
* transitions. Pushing that back down as a "disable"/"clear" command flips native
|
|
160
|
+
* state, which emits a fresh onTracksChanged, which we would echo again — an
|
|
161
|
+
* infinite feedback loop.
|
|
162
|
+
*
|
|
163
|
+
* Keep the current selection when the reported value is a transient drop and the
|
|
164
|
+
* chosen track is still available. A genuine user "Off"/change already updates the
|
|
165
|
+
* controlled state first, so this only ignores unsolicited native status blips.
|
|
166
|
+
*/
|
|
167
|
+
const resolveReportedTrackId = (
|
|
168
|
+
reportedId: string | null | undefined,
|
|
169
|
+
currentId: string | null,
|
|
170
|
+
tracks: { id: string }[] = []
|
|
171
|
+
): string | null => {
|
|
172
|
+
if (reportedId) {
|
|
173
|
+
return reportedId;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (currentId && tracks.some((track) => track?.id === currentId)) {
|
|
177
|
+
return currentId;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
182
|
+
|
|
99
183
|
export default class VideoPlayer extends React.Component<
|
|
100
184
|
VideoPlayerProps,
|
|
101
185
|
QuickBrickPlayer.NativePlayerState & {
|
|
@@ -164,20 +248,10 @@ export default class VideoPlayer extends React.Component<
|
|
|
164
248
|
nativePlayerIsReady: false,
|
|
165
249
|
};
|
|
166
250
|
|
|
167
|
-
private _tracker: PlayerAnalyticsTrackerI;
|
|
168
|
-
get analyticsTracker() {
|
|
169
|
-
if (!this._tracker) {
|
|
170
|
-
this._tracker = PlayerAnalyticsManager.getInstance().getTracker(
|
|
171
|
-
this.props.playerId
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return this._tracker;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
251
|
constructor(props) {
|
|
179
252
|
super(props);
|
|
180
253
|
this.onVideoLoadCalled = false; // android
|
|
254
|
+
PlayerAnalyticsManager.getInstance();
|
|
181
255
|
|
|
182
256
|
this.takeParentStyle = isWeb()
|
|
183
257
|
? { width: "100%", flex: "1 1 auto", objectFit: "contain" }
|
|
@@ -191,9 +265,6 @@ export default class VideoPlayer extends React.Component<
|
|
|
191
265
|
componentWillUnmount() {
|
|
192
266
|
this.playbackStarted = false;
|
|
193
267
|
|
|
194
|
-
this.analyticsTracker.handleAnalyticEvent(GENERAL_EVENT.session_end);
|
|
195
|
-
this.onPlayerClosed();
|
|
196
|
-
|
|
197
268
|
if (isAndroidPlatform()) {
|
|
198
269
|
this.disablePiP();
|
|
199
270
|
}
|
|
@@ -201,7 +272,6 @@ export default class VideoPlayer extends React.Component<
|
|
|
201
272
|
|
|
202
273
|
componentDidMount() {
|
|
203
274
|
this.playbackStarted = false;
|
|
204
|
-
this.onPlayerPresented();
|
|
205
275
|
}
|
|
206
276
|
|
|
207
277
|
shouldComponentUpdate(nextProps) {
|
|
@@ -445,14 +515,6 @@ export default class VideoPlayer extends React.Component<
|
|
|
445
515
|
});
|
|
446
516
|
};
|
|
447
517
|
|
|
448
|
-
onPlayerPresented = () => {
|
|
449
|
-
this.analyticsTracker?.handleAnalyticEvent(GENERAL_EVENT.player_presented);
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
onPlayerClosed = () => {
|
|
453
|
-
this.analyticsTracker?.handleAnalyticEvent(GENERAL_EVENT.player_closed);
|
|
454
|
-
};
|
|
455
|
-
|
|
456
518
|
onVideoEnd = () => {
|
|
457
519
|
this.setState({ displayOverlay: !this.state.displayOverlay }, () => {
|
|
458
520
|
if (this.state?.displayOverlay) {
|
|
@@ -667,20 +729,50 @@ export default class VideoPlayer extends React.Component<
|
|
|
667
729
|
const { textTracks, audioTracks, audioTrackId, textTrackId } =
|
|
668
730
|
parseLanguageTracks(nativeEvent);
|
|
669
731
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
textTracks,
|
|
678
|
-
audioTracks,
|
|
679
|
-
audioTrackId,
|
|
680
|
-
textTrackId,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
732
|
+
// Resolve the reported selection against the current controlled state so a
|
|
733
|
+
// transient native "nothing selected" doesn't get echoed back as a command
|
|
734
|
+
// and re-trigger native selection (infinite loop, see resolveReportedTrackId).
|
|
735
|
+
const nextAudioTrackId = resolveReportedTrackId(
|
|
736
|
+
audioTrackId,
|
|
737
|
+
this.state.audioTrackId,
|
|
738
|
+
audioTracks
|
|
683
739
|
);
|
|
740
|
+
|
|
741
|
+
const nextTextTrackId = resolveReportedTrackId(
|
|
742
|
+
textTrackId,
|
|
743
|
+
this.state.textTrackId,
|
|
744
|
+
textTracks
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const notifyListener = () =>
|
|
748
|
+
this.props?.listener?.onTracksChanged?.({
|
|
749
|
+
textTracks,
|
|
750
|
+
audioTracks,
|
|
751
|
+
audioTrackId: nextAudioTrackId,
|
|
752
|
+
textTrackId: nextTextTrackId,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Only update state (and thus re-send the controlled props) on a real change.
|
|
756
|
+
// Re-rendering with identical prop values would otherwise keep the native
|
|
757
|
+
// event/prop echo alive.
|
|
758
|
+
const stateChanges: {
|
|
759
|
+
audioTrackId?: string | null;
|
|
760
|
+
textTrackId?: string | null;
|
|
761
|
+
} = {};
|
|
762
|
+
|
|
763
|
+
if (nextAudioTrackId !== this.state.audioTrackId) {
|
|
764
|
+
stateChanges.audioTrackId = nextAudioTrackId;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (nextTextTrackId !== this.state.textTrackId) {
|
|
768
|
+
stateChanges.textTrackId = nextTextTrackId;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (Object.keys(stateChanges).length > 0) {
|
|
772
|
+
this.setState(stateChanges as any, notifyListener);
|
|
773
|
+
} else {
|
|
774
|
+
notifyListener();
|
|
775
|
+
}
|
|
684
776
|
};
|
|
685
777
|
|
|
686
778
|
onVideoProgress = ({ nativeEvent }) => {
|
|
@@ -1096,6 +1188,15 @@ export default class VideoPlayer extends React.Component<
|
|
|
1096
1188
|
const liveCatchUpEnabled =
|
|
1097
1189
|
this.props.pluginConfiguration?.liveCatchUpEnabled;
|
|
1098
1190
|
|
|
1191
|
+
// Look the player up by id rather than using the active player: these
|
|
1192
|
+
// props are handed to this component's own native player view, so they
|
|
1193
|
+
// must describe that player's content even while a cast session is
|
|
1194
|
+
// running (during which `usePlayer` swaps React consumers over to the
|
|
1195
|
+
// casting receiver).
|
|
1196
|
+
const metadata = toNativeMetadata(
|
|
1197
|
+
playerManager.getPlayerWithId(this.props.playerId)?.getContent()
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1099
1200
|
const nativeProps = {
|
|
1100
1201
|
rate: undefined,
|
|
1101
1202
|
entry,
|
|
@@ -1114,6 +1215,7 @@ export default class VideoPlayer extends React.Component<
|
|
|
1114
1215
|
liveCatchUpEnabled,
|
|
1115
1216
|
playerId: this.props.playerId,
|
|
1116
1217
|
pictureInPictureEnabled,
|
|
1218
|
+
metadata,
|
|
1117
1219
|
seekStep:
|
|
1118
1220
|
(this.props.pluginConfiguration?.seek_duration || SEEK_TIME) * 1000,
|
|
1119
1221
|
accessibilityProps: getAllAccessibilityProps(
|
|
@@ -1170,10 +1272,15 @@ export default class VideoPlayer extends React.Component<
|
|
|
1170
1272
|
return null;
|
|
1171
1273
|
}
|
|
1172
1274
|
|
|
1173
|
-
const
|
|
1275
|
+
const isTvPlatform = isTV();
|
|
1276
|
+
|
|
1277
|
+
const TransportControls = isTvPlatform
|
|
1174
1278
|
? TransportControlsTV
|
|
1175
1279
|
: TransportControlsMobile;
|
|
1176
1280
|
|
|
1281
|
+
// The mobile and TV transport controls read Title/Subtitle from the
|
|
1282
|
+
// player manager's content channel (via `usePlayerContent`) instead of
|
|
1283
|
+
// receiving them as props.
|
|
1177
1284
|
const needToShowDefaultControls = !(
|
|
1178
1285
|
(controls && !castAction?.state?.connected) ||
|
|
1179
1286
|
this.getPlayNextData()
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { ModalHeader } from "@applicaster/zapp-react-native-ui-components/Components/ModalComponent/Header";
|
|
3
|
-
import { isNil } from "@applicaster/zapp-react-native-utils/utils";
|
|
4
|
-
|
|
5
|
-
const formatTime = (milliseconds: number) => {
|
|
6
|
-
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
7
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
8
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
9
|
-
const seconds = totalSeconds % 60;
|
|
10
|
-
|
|
11
|
-
if (milliseconds < 3600000) {
|
|
12
|
-
return `${minutes.toString().padStart(2, "0")}:${seconds
|
|
13
|
-
.toString()
|
|
14
|
-
.padStart(2, "0")}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return `${hours.toString().padStart(2, "0")}:${minutes
|
|
18
|
-
.toString()
|
|
19
|
-
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const SleepTimerModalHeader = ({
|
|
23
|
-
summary,
|
|
24
|
-
...props
|
|
25
|
-
}: {
|
|
26
|
-
summary?: string;
|
|
27
|
-
}) => {
|
|
28
|
-
const sleepTimerDate = +summary;
|
|
29
|
-
|
|
30
|
-
const [timeLeft, setTimeLeft] = React.useState(
|
|
31
|
-
sleepTimerDate !== 0 ? sleepTimerDate - Date.now() : 0
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const showCounter = React.useMemo(() => sleepTimerDate > 0, [sleepTimerDate]);
|
|
35
|
-
|
|
36
|
-
React.useLayoutEffect(() => {
|
|
37
|
-
if (showCounter && sleepTimerDate !== 0) {
|
|
38
|
-
setTimeLeft(sleepTimerDate - Date.now());
|
|
39
|
-
|
|
40
|
-
const intervalId = setInterval(() => {
|
|
41
|
-
setTimeLeft(() => {
|
|
42
|
-
const newTimeLeft = sleepTimerDate - Date.now();
|
|
43
|
-
|
|
44
|
-
if (newTimeLeft <= 0) {
|
|
45
|
-
clearInterval(intervalId);
|
|
46
|
-
|
|
47
|
-
return 0;
|
|
48
|
-
} else {
|
|
49
|
-
return newTimeLeft;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
}, 1000);
|
|
53
|
-
|
|
54
|
-
return () => clearInterval(intervalId);
|
|
55
|
-
} else {
|
|
56
|
-
setTimeLeft(0);
|
|
57
|
-
}
|
|
58
|
-
}, [sleepTimerDate, showCounter]);
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<ModalHeader
|
|
62
|
-
summary={!isNil(summary) ? formatTime(timeLeft) : summary}
|
|
63
|
-
{...(props as any)}
|
|
64
|
-
/>
|
|
65
|
-
);
|
|
66
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./SleepTimerModalHeader";
|