@applicaster/quick-brick-player 16.0.0-rc.11 → 16.0.0-rc.111
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 +354 -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 +528 -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 +539 -0
- package/src/Player/AudioLayer/playerActions.ts +294 -0
- package/src/Player/AudioLayer/usePlaylistNavigation.ts +148 -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 +168 -40
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +0 -66
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +0 -1
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) {
|
|
@@ -328,6 +398,22 @@ export default class VideoPlayer extends React.Component<
|
|
|
328
398
|
RNPlayerManager?.[this.playerPluginId]?.stopBackgroundPlayback?.();
|
|
329
399
|
};
|
|
330
400
|
|
|
401
|
+
command = (command: string, userInfo?: Record<string, any>) => {
|
|
402
|
+
if (
|
|
403
|
+
this.isReactNativeMethodsSupported() &&
|
|
404
|
+
RNPlayerManager?.[this.playerPluginId]?.command &&
|
|
405
|
+
this._root
|
|
406
|
+
) {
|
|
407
|
+
const node = findNodeHandle(this._root);
|
|
408
|
+
|
|
409
|
+
if (!node) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
RNPlayerManager?.[this.playerPluginId]?.command(node, command, userInfo);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
331
417
|
forward = (seekTime = SEEK_TIME) => {
|
|
332
418
|
const currentTime = this.state.currentTime;
|
|
333
419
|
|
|
@@ -445,14 +531,6 @@ export default class VideoPlayer extends React.Component<
|
|
|
445
531
|
});
|
|
446
532
|
};
|
|
447
533
|
|
|
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
534
|
onVideoEnd = () => {
|
|
457
535
|
this.setState({ displayOverlay: !this.state.displayOverlay }, () => {
|
|
458
536
|
if (this.state?.displayOverlay) {
|
|
@@ -473,6 +551,10 @@ export default class VideoPlayer extends React.Component<
|
|
|
473
551
|
return this.props?.listener?.onError(newError);
|
|
474
552
|
};
|
|
475
553
|
|
|
554
|
+
onRemoteCommand = ({ nativeEvent = null }) => {
|
|
555
|
+
this.props?.listener?.onRemoteCommand?.(nativeEvent);
|
|
556
|
+
};
|
|
557
|
+
|
|
476
558
|
onPlayerPause = ({ nativeEvent = null }) => {
|
|
477
559
|
this.setState({ paused: true });
|
|
478
560
|
this.props?.listener?.onPlayerPause(nativeEvent);
|
|
@@ -667,20 +749,50 @@ export default class VideoPlayer extends React.Component<
|
|
|
667
749
|
const { textTracks, audioTracks, audioTrackId, textTrackId } =
|
|
668
750
|
parseLanguageTracks(nativeEvent);
|
|
669
751
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
textTracks,
|
|
678
|
-
audioTracks,
|
|
679
|
-
audioTrackId,
|
|
680
|
-
textTrackId,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
752
|
+
// Resolve the reported selection against the current controlled state so a
|
|
753
|
+
// transient native "nothing selected" doesn't get echoed back as a command
|
|
754
|
+
// and re-trigger native selection (infinite loop, see resolveReportedTrackId).
|
|
755
|
+
const nextAudioTrackId = resolveReportedTrackId(
|
|
756
|
+
audioTrackId,
|
|
757
|
+
this.state.audioTrackId,
|
|
758
|
+
audioTracks
|
|
683
759
|
);
|
|
760
|
+
|
|
761
|
+
const nextTextTrackId = resolveReportedTrackId(
|
|
762
|
+
textTrackId,
|
|
763
|
+
this.state.textTrackId,
|
|
764
|
+
textTracks
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
const notifyListener = () =>
|
|
768
|
+
this.props?.listener?.onTracksChanged?.({
|
|
769
|
+
textTracks,
|
|
770
|
+
audioTracks,
|
|
771
|
+
audioTrackId: nextAudioTrackId,
|
|
772
|
+
textTrackId: nextTextTrackId,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// Only update state (and thus re-send the controlled props) on a real change.
|
|
776
|
+
// Re-rendering with identical prop values would otherwise keep the native
|
|
777
|
+
// event/prop echo alive.
|
|
778
|
+
const stateChanges: {
|
|
779
|
+
audioTrackId?: string | null;
|
|
780
|
+
textTrackId?: string | null;
|
|
781
|
+
} = {};
|
|
782
|
+
|
|
783
|
+
if (nextAudioTrackId !== this.state.audioTrackId) {
|
|
784
|
+
stateChanges.audioTrackId = nextAudioTrackId;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (nextTextTrackId !== this.state.textTrackId) {
|
|
788
|
+
stateChanges.textTrackId = nextTextTrackId;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (Object.keys(stateChanges).length > 0) {
|
|
792
|
+
this.setState(stateChanges as any, notifyListener);
|
|
793
|
+
} else {
|
|
794
|
+
notifyListener();
|
|
795
|
+
}
|
|
684
796
|
};
|
|
685
797
|
|
|
686
798
|
onVideoProgress = ({ nativeEvent }) => {
|
|
@@ -977,6 +1089,7 @@ export default class VideoPlayer extends React.Component<
|
|
|
977
1089
|
onPlayerResume: this.onPlayerResume,
|
|
978
1090
|
onPlayerSeekComplete: this.onPlayerSeekComplete,
|
|
979
1091
|
onPlayerSeekStart: this.onPlayerSeekStart,
|
|
1092
|
+
onRemoteCommand: this.onRemoteCommand,
|
|
980
1093
|
};
|
|
981
1094
|
|
|
982
1095
|
const lifecycleEvents = {
|
|
@@ -1096,6 +1209,15 @@ export default class VideoPlayer extends React.Component<
|
|
|
1096
1209
|
const liveCatchUpEnabled =
|
|
1097
1210
|
this.props.pluginConfiguration?.liveCatchUpEnabled;
|
|
1098
1211
|
|
|
1212
|
+
// Look the player up by id rather than using the active player: these
|
|
1213
|
+
// props are handed to this component's own native player view, so they
|
|
1214
|
+
// must describe that player's content even while a cast session is
|
|
1215
|
+
// running (during which `usePlayer` swaps React consumers over to the
|
|
1216
|
+
// casting receiver).
|
|
1217
|
+
const metadata = toNativeMetadata(
|
|
1218
|
+
playerManager.getPlayerWithId(this.props.playerId)?.getContent()
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1099
1221
|
const nativeProps = {
|
|
1100
1222
|
rate: undefined,
|
|
1101
1223
|
entry,
|
|
@@ -1114,6 +1236,7 @@ export default class VideoPlayer extends React.Component<
|
|
|
1114
1236
|
liveCatchUpEnabled,
|
|
1115
1237
|
playerId: this.props.playerId,
|
|
1116
1238
|
pictureInPictureEnabled,
|
|
1239
|
+
metadata,
|
|
1117
1240
|
seekStep:
|
|
1118
1241
|
(this.props.pluginConfiguration?.seek_duration || SEEK_TIME) * 1000,
|
|
1119
1242
|
accessibilityProps: getAllAccessibilityProps(
|
|
@@ -1170,10 +1293,15 @@ export default class VideoPlayer extends React.Component<
|
|
|
1170
1293
|
return null;
|
|
1171
1294
|
}
|
|
1172
1295
|
|
|
1173
|
-
const
|
|
1296
|
+
const isTvPlatform = isTV();
|
|
1297
|
+
|
|
1298
|
+
const TransportControls = isTvPlatform
|
|
1174
1299
|
? TransportControlsTV
|
|
1175
1300
|
: TransportControlsMobile;
|
|
1176
1301
|
|
|
1302
|
+
// The mobile and TV transport controls read Title/Subtitle from the
|
|
1303
|
+
// player manager's content channel (via `usePlayerContent`) instead of
|
|
1304
|
+
// receiving them as props.
|
|
1177
1305
|
const needToShowDefaultControls = !(
|
|
1178
1306
|
(controls && !castAction?.state?.connected) ||
|
|
1179
1307
|
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";
|