@applicaster/quick-brick-player 13.0.0-rc.13 → 13.0.0-rc.131
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 +27 -14
- package/src/CellPlayer/Assets/AssetsComponent.tsx +17 -3
- package/src/Player/AudioLayer/AudioDescription.tsx +107 -68
- package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +142 -295
- package/src/Player/AudioLayer/Layout/Controls/VerticalGutter.tsx +45 -0
- package/src/Player/AudioLayer/Layout/Controls/index.tsx +291 -0
- package/src/Player/AudioLayer/Layout/{defaultLayoutsConts.ts → Controls/utils.ts} +9 -27
- package/src/Player/AudioLayer/Layout/Debug/index.tsx +55 -0
- package/src/Player/AudioLayer/Layout/{DockedLayout.tsx → DockedControls/index.tsx} +77 -81
- package/src/Player/AudioLayer/Layout/MobileLayout.tsx +222 -0
- package/src/Player/AudioLayer/Layout/PlayerImage/AnimatedImage.tsx +87 -0
- package/src/Player/AudioLayer/Layout/PlayerImage/FlexImage.tsx +20 -0
- package/src/Player/AudioLayer/Layout/PlayerImage/hooks/index.ts +1 -0
- package/src/Player/AudioLayer/Layout/PlayerImage/hooks/useChangePlayerState.ts +99 -0
- package/src/Player/AudioLayer/Layout/PlayerImage/index.tsx +112 -0
- package/src/Player/AudioLayer/Layout/PlayerImage/styles.ts +36 -0
- package/src/Player/AudioLayer/Layout/TabletLandscapeLayout.tsx +212 -0
- package/src/Player/AudioLayer/Layout/TabletPortraitLayout.tsx +215 -0
- package/src/Player/AudioLayer/Layout/TransportInfo/index.tsx +128 -0
- package/src/Player/AudioLayer/Layout/VerticalGutter/index.tsx +8 -0
- package/src/Player/AudioLayer/Layout/const.ts +26 -0
- package/src/Player/AudioLayer/Layout/debug.ts +6 -0
- package/src/Player/AudioLayer/Layout/hooks/index.ts +5 -0
- package/src/Player/AudioLayer/Layout/hooks/useMobileLayoutImageSize.ts +14 -0
- package/src/Player/AudioLayer/Layout/hooks/useTabletLayoutImageSize.ts +105 -0
- package/src/Player/AudioLayer/Layout/hooks/useTransportInfoHeight.ts +54 -0
- package/src/Player/AudioLayer/Layout/index.tsx +21 -48
- package/src/Player/AudioLayer/Layout/types.ts +7 -0
- package/src/Player/AudioLayer/Layout/utils.ts +7 -0
- package/src/Player/AudioLayer/SleepTimerManager.ts +130 -0
- package/src/Player/AudioLayer/SpeedManager.ts +52 -0
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +66 -0
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +1 -0
- package/src/Player/AudioLayer/useSleepTimerModal.ts +109 -0
- package/src/Player/AudioLayer/utils.ts +27 -0
- package/src/Player/Const/sleepTimerOptions.ts +41 -0
- package/src/Player/Const/speedOptions.ts +4 -0
- package/src/Player/Utils/Utils.ts +0 -5
- package/src/Player/Utils/index.tsx +123 -0
- package/src/Player/hooks/useIsCasting.tsx +57 -0
- package/src/Player/index.tsx +106 -49
- package/src/QuickBrickPlayer.tsx +3 -0
- package/src/Player/AudioLayer/Layout/DefaultLayout.tsx +0 -452
- package/src/Player/AudioLayer/LinearGradient/LinearGradientProxy.ts +0 -3
- package/src/Player/AudioLayer/LinearGradient/LinearGradientProxy.web.ts +0 -3
- package/src/Player/AudioLayer/LinearGradient/index.ts +0 -1
- package/src/Player/AudioLayer/MagicBG/index.tsx +0 -155
- package/src/Player/Helpers/VideoResizeMode.ts +0 -7
- package/src/utils/utils.ts +0 -25
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { LayoutChangeEvent } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { toNumber } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
5
|
+
import { Subject } from "rxjs";
|
|
6
|
+
import { first, debounceTime, filter } from "rxjs/operators";
|
|
7
|
+
import { useRefWithInitialValue } from "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue";
|
|
8
|
+
|
|
9
|
+
const TIMEOUT = 200; // ms
|
|
10
|
+
|
|
11
|
+
type Layout = {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const builder = () => {
|
|
19
|
+
const layoutChange$ = new Subject<Layout>();
|
|
20
|
+
|
|
21
|
+
const emitLayoutChange = (layout: Layout): void => {
|
|
22
|
+
layoutChange$.next(layout);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
emitLayoutChange,
|
|
27
|
+
layoutChange$,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Props = {
|
|
32
|
+
isPortraitMode: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type Return = {
|
|
36
|
+
imageSize: Option<number>;
|
|
37
|
+
offsetY: Option<number>;
|
|
38
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const useTabletLayoutImageSize = ({ isPortraitMode }: Props): Return => {
|
|
42
|
+
const [imageSize, setImageSize] = React.useState(undefined);
|
|
43
|
+
const [offsetY, setOffsetY] = React.useState(undefined);
|
|
44
|
+
|
|
45
|
+
const layoutRef = useRefWithInitialValue<ReturnType<typeof builder>>(builder);
|
|
46
|
+
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
const subscription = layoutRef.current.layoutChange$
|
|
49
|
+
// for landscape layout we need to wait the second layout change in order to get proper size of image
|
|
50
|
+
.pipe(debounceTime(TIMEOUT), first())
|
|
51
|
+
.subscribe({
|
|
52
|
+
next: (layout: Layout) => {
|
|
53
|
+
const size = isPortraitMode
|
|
54
|
+
? Math.min(layout.height, layout.width)
|
|
55
|
+
: layout.height;
|
|
56
|
+
|
|
57
|
+
setImageSize(size);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
subscription.unsubscribe();
|
|
63
|
+
};
|
|
64
|
+
}, [isPortraitMode]);
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
const subscription = layoutRef.current.layoutChange$
|
|
68
|
+
.pipe(
|
|
69
|
+
filter((layout: Layout) => layout.y > 0),
|
|
70
|
+
debounceTime(TIMEOUT),
|
|
71
|
+
first()
|
|
72
|
+
)
|
|
73
|
+
.subscribe({
|
|
74
|
+
next: (layout: Layout) => {
|
|
75
|
+
setOffsetY(layout.y);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
subscription.unsubscribe();
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const handleLayout = React.useCallback((event: LayoutChangeEvent) => {
|
|
85
|
+
const height = toNumber(event?.nativeEvent?.layout?.height);
|
|
86
|
+
const width = toNumber(event?.nativeEvent?.layout?.width);
|
|
87
|
+
const y = toNumber(event?.nativeEvent?.layout?.y);
|
|
88
|
+
const x = toNumber(event?.nativeEvent?.layout?.x);
|
|
89
|
+
|
|
90
|
+
const layout: Layout = {
|
|
91
|
+
x,
|
|
92
|
+
y,
|
|
93
|
+
width,
|
|
94
|
+
height,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
layoutRef.current.emitLayoutChange(layout);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
imageSize,
|
|
102
|
+
offsetY,
|
|
103
|
+
onLayout: handleLayout,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { LayoutChangeEvent } from "react-native";
|
|
3
|
+
import { toNumber } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
4
|
+
import { Subject } from "rxjs";
|
|
5
|
+
import { first } from "rxjs/operators";
|
|
6
|
+
import { useRefWithInitialValue } from "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue";
|
|
7
|
+
|
|
8
|
+
const builder = () => {
|
|
9
|
+
const layoutChange$ = new Subject<number>();
|
|
10
|
+
|
|
11
|
+
const emitLayoutChange = (height: number): void => {
|
|
12
|
+
layoutChange$.next(height);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
emitLayoutChange,
|
|
17
|
+
layoutChange$,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const useTransportInfoHeight = () => {
|
|
22
|
+
const [height, setHeight] = React.useState(0);
|
|
23
|
+
|
|
24
|
+
const layoutRef = useRefWithInitialValue<ReturnType<typeof builder>>(builder);
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
const subscription = layoutRef.current.layoutChange$
|
|
28
|
+
// skip first layout and take the second with proper size
|
|
29
|
+
// then ignore any layout update
|
|
30
|
+
.pipe(first())
|
|
31
|
+
.subscribe({
|
|
32
|
+
next: (size: number) => {
|
|
33
|
+
setHeight(size);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
subscription.unsubscribe();
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleLayout = React.useCallback((event: LayoutChangeEvent) => {
|
|
43
|
+
const height = toNumber(event?.nativeEvent?.layout?.height);
|
|
44
|
+
|
|
45
|
+
if (height) {
|
|
46
|
+
layoutRef.current.emitLayoutChange(height);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
height,
|
|
52
|
+
onLayout: handleLayout,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -1,52 +1,25 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import DefaultLayout from "./DefaultLayout";
|
|
3
|
-
import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
|
|
1
|
+
import * as React from "react";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
height: number;
|
|
8
|
-
};
|
|
3
|
+
import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
4
|
+
import { useIsTabletLandscape } from "@applicaster/zapp-react-native-utils/reactHooks/device/useMemoizedIsTablet";
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
inline: boolean;
|
|
14
|
-
docked: boolean;
|
|
15
|
-
isModal: boolean;
|
|
16
|
-
};
|
|
17
|
-
playerState: {
|
|
18
|
-
currentTime: number;
|
|
19
|
-
duration: number;
|
|
20
|
-
seekableDuration: number;
|
|
21
|
-
isLive: boolean;
|
|
22
|
-
isPaused: boolean;
|
|
23
|
-
isBuffering: boolean;
|
|
24
|
-
};
|
|
25
|
-
value: GetValue;
|
|
26
|
-
sleepTimerDate: number;
|
|
27
|
-
playbackSpeedName: string;
|
|
28
|
-
playbackSleepName: string;
|
|
29
|
-
configuration: { [key: string]: any };
|
|
30
|
-
onPlaybackButtonPress: () => void;
|
|
31
|
-
onRewindButtonPress: () => void;
|
|
32
|
-
onForwardButtonPress: () => void;
|
|
33
|
-
onSpeedButtonPress: () => void;
|
|
34
|
-
onSleepButtonPress: () => void;
|
|
35
|
-
onCloseVideoModal: () => void;
|
|
36
|
-
cancelSleepTimer: () => void;
|
|
37
|
-
onPlayerSeek: (targetTime: number) => void;
|
|
38
|
-
onSkipStart: () => void;
|
|
39
|
-
onSkipLive: () => void;
|
|
40
|
-
maximiseVideoModal: () => void;
|
|
41
|
-
dimensions: Dimensions;
|
|
42
|
-
defaultDimensions: Dimensions;
|
|
43
|
-
playNextData: PlayNextData;
|
|
44
|
-
title: string | number;
|
|
45
|
-
summary: string | number;
|
|
46
|
-
};
|
|
6
|
+
import { TabletPortraitLayout } from "./TabletPortraitLayout";
|
|
7
|
+
import { TabletLandscapeLayout } from "./TabletLandscapeLayout";
|
|
8
|
+
import { MobileLayout } from "./MobileLayout";
|
|
47
9
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
10
|
+
import { Props } from "./types";
|
|
11
|
+
|
|
12
|
+
export default function Layout(props: Props) {
|
|
13
|
+
const isTablet = useIsTablet();
|
|
14
|
+
const isTabletLandscape = useIsTabletLandscape();
|
|
15
|
+
|
|
16
|
+
if (!isTablet) {
|
|
17
|
+
return <MobileLayout {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (isTabletLandscape) {
|
|
21
|
+
return <TabletLandscapeLayout {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return <TabletPortraitLayout {...props} />;
|
|
52
25
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether the screen height is matches with our threshold for small tablets.
|
|
3
|
+
* @param height screen height
|
|
4
|
+
* @returns boolean indicating if the height is less than 800 pixels
|
|
5
|
+
*/
|
|
6
|
+
export const isSmallTabletHeight = (height: number): boolean =>
|
|
7
|
+
!(height >= 800);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
3
|
+
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
4
|
+
import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
|
|
5
|
+
import { hasDatePassed, NAMESPACE } from "./utils";
|
|
6
|
+
|
|
7
|
+
const { log_debug, log_error } = createLogger({
|
|
8
|
+
subsystem: "Player",
|
|
9
|
+
category: "SleepTimerManager",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type SleepTimer = {
|
|
13
|
+
label: string;
|
|
14
|
+
// interval in minutes for UI at the moment when timer was first started
|
|
15
|
+
value: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const SLEEP_TIMER_OFF: SleepTimer = {
|
|
19
|
+
label: "playback_sleep_off_label",
|
|
20
|
+
value: 0,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class SleepTimerState {
|
|
24
|
+
constructor(
|
|
25
|
+
public timer: SleepTimer,
|
|
26
|
+
public timerDate: number
|
|
27
|
+
) {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SLEEP_PREFERENCE_KEY = "sleep";
|
|
31
|
+
|
|
32
|
+
export class SleepTimerController {
|
|
33
|
+
// TODO cancel timer on reach time
|
|
34
|
+
private sleepTimerStateSubject = new BehaviorSubject<SleepTimerState>(
|
|
35
|
+
new SleepTimerState(SLEEP_TIMER_OFF, 0)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
constructor(private player: Player) {}
|
|
39
|
+
|
|
40
|
+
get sleepTimerState$() {
|
|
41
|
+
return this.sleepTimerStateSubject.asObservable();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async scheduleSleep(sleepDate): Promise<boolean> {
|
|
45
|
+
log_debug(`Setting sleep interval to ${sleepDate}`);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
this.player.cancelSleepTimer?.();
|
|
49
|
+
this.player.startSleepTimer?.(sleepDate);
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
log_error("Failed to start sleep timer", { error: e.message });
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async setSleepTimer(item: SleepTimer, prevTimerSleepDate?: number) {
|
|
60
|
+
// User has selected Off state
|
|
61
|
+
if (item.value === 0) {
|
|
62
|
+
log_debug("Cancelling sleep timer");
|
|
63
|
+
|
|
64
|
+
// Removing scheduled pause on timer expiration
|
|
65
|
+
this.player.cancelSleepTimer?.();
|
|
66
|
+
|
|
67
|
+
return this.onTimerExpired();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const futureSleepDate =
|
|
71
|
+
prevTimerSleepDate || Date.now() + item.value * 60000;
|
|
72
|
+
|
|
73
|
+
log_debug(
|
|
74
|
+
`Starting sleep timer in ${(futureSleepDate - Date.now()) / 1000}s`
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (await this.scheduleSleep(futureSleepDate)) {
|
|
78
|
+
await localStorage.setItem(
|
|
79
|
+
SLEEP_PREFERENCE_KEY,
|
|
80
|
+
{ ...item, futureSleepDate },
|
|
81
|
+
NAMESPACE
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
this.sleepTimerStateSubject.next(
|
|
85
|
+
new SleepTimerState(item, futureSleepDate)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async onTimerExpired() {
|
|
91
|
+
this.removeSleepTimerFromLocalStorage().catch((error) => {
|
|
92
|
+
log_error("Failed to remove sleep timer from local storage", {
|
|
93
|
+
error: error.message,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Update UI state
|
|
98
|
+
this.sleepTimerStateSubject.next(new SleepTimerState(SLEEP_TIMER_OFF, 0));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async removeSleepTimerFromLocalStorage() {
|
|
102
|
+
await localStorage.removeItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async loadSleep() {
|
|
106
|
+
const sleep = await localStorage.getItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
|
|
107
|
+
|
|
108
|
+
if (!sleep) return;
|
|
109
|
+
|
|
110
|
+
const { futureSleepDate, ...item } = sleep;
|
|
111
|
+
|
|
112
|
+
if (!futureSleepDate || hasDatePassed(futureSleepDate)) {
|
|
113
|
+
log_debug("Sleep date has already passed");
|
|
114
|
+
await localStorage.removeItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
|
|
115
|
+
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log_debug("Sleep state restored");
|
|
120
|
+
|
|
121
|
+
this.setSleepTimer(item as SleepTimer, futureSleepDate);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Will be called if user closed the player explicitly.
|
|
125
|
+
// TODO: check if it will be called on Android when we need.
|
|
126
|
+
// Probably we will have to handle it on native instead.
|
|
127
|
+
public static async onPlayerDestroy() {
|
|
128
|
+
await localStorage.removeItem(SLEEP_PREFERENCE_KEY, NAMESPACE);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
3
|
+
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
4
|
+
import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
|
|
5
|
+
import { NAMESPACE } from "./utils";
|
|
6
|
+
|
|
7
|
+
const { log_debug } = createLogger({
|
|
8
|
+
subsystem: "Player",
|
|
9
|
+
category: "PlaybackSpeedManager",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type PlaybackSpeed = {
|
|
13
|
+
asset: string;
|
|
14
|
+
value: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SPEED_PREFERENCE_KEY = "speed";
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_SPEED: PlaybackSpeed = {
|
|
20
|
+
asset: "speed_1",
|
|
21
|
+
value: 1.0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// todo: should not be applied to live streams
|
|
25
|
+
export class SpeedController {
|
|
26
|
+
private playbackSpeedSubject = new BehaviorSubject<PlaybackSpeed>(
|
|
27
|
+
DEFAULT_SPEED
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
constructor(private player: Player) {}
|
|
31
|
+
|
|
32
|
+
get playbackSpeed$() {
|
|
33
|
+
return this.playbackSpeedSubject.asObservable();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async loadSpeed() {
|
|
37
|
+
const speed = await localStorage.getItem(SPEED_PREFERENCE_KEY, NAMESPACE);
|
|
38
|
+
|
|
39
|
+
// checking for value in case we will pull empty or broken object
|
|
40
|
+
if (speed && speed.value) {
|
|
41
|
+
this.playbackSpeedSubject.next(speed);
|
|
42
|
+
this.player.setPlaybackRate?.(speed.value);
|
|
43
|
+
log_debug(`Speed loaded: ${speed}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async setPlaybackSpeed(speed: PlaybackSpeed) {
|
|
48
|
+
await localStorage.setItem(SPEED_PREFERENCE_KEY, speed, NAMESPACE);
|
|
49
|
+
this.playbackSpeedSubject.next(speed);
|
|
50
|
+
this.player.setPlaybackRate?.(speed.value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ModalHeader } from "@applicaster/zapp-react-native-ui-components/Components/ModalComponent/Header";
|
|
3
|
+
import { isNil } from "lodash";
|
|
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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./SleepTimerModalHeader";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
SLEEP_TIMER_OFF,
|
|
5
|
+
SleepTimerController,
|
|
6
|
+
SleepTimerState,
|
|
7
|
+
} from "./SleepTimerManager";
|
|
8
|
+
|
|
9
|
+
import { openBottomSheetModal } from "@applicaster/zapp-react-native-utils/modalState";
|
|
10
|
+
|
|
11
|
+
import { SleepTimerModalHeader } from "./components/SleepTimerModalHeader";
|
|
12
|
+
import { getSleepTimerOptions } from "../Const/sleepTimerOptions";
|
|
13
|
+
import {
|
|
14
|
+
getBottomSheetModalLabelProps,
|
|
15
|
+
getBottomSheetModalIconProps,
|
|
16
|
+
getBottomSheetModalItemProps,
|
|
17
|
+
getBottomSheetModalSelectedItemIcon,
|
|
18
|
+
selectSleepTitle,
|
|
19
|
+
selectSleepOffLabel,
|
|
20
|
+
} from "../Utils";
|
|
21
|
+
|
|
22
|
+
export const useSleepTimerModal = (player, configuration) => {
|
|
23
|
+
const playerState = usePlayerState("audio-player-sleep-timer-modal");
|
|
24
|
+
|
|
25
|
+
const title = configuration(selectSleepTitle);
|
|
26
|
+
const offLabel = configuration(selectSleepOffLabel);
|
|
27
|
+
|
|
28
|
+
const [sleepTimerManager, setSleepTimerManager] =
|
|
29
|
+
useState<SleepTimerController | null>(null);
|
|
30
|
+
|
|
31
|
+
const [sleepTimerState, setSleepTimerState] = useState<SleepTimerState>(
|
|
32
|
+
new SleepTimerState(SLEEP_TIMER_OFF, 0)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (player) {
|
|
37
|
+
setSleepTimerManager(new SleepTimerController(player));
|
|
38
|
+
} else {
|
|
39
|
+
setSleepTimerManager(null);
|
|
40
|
+
}
|
|
41
|
+
}, [player]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (playerState?.isLive) {
|
|
45
|
+
// Resetting a sleep timer when audio content is live (Timer not supported on live video)
|
|
46
|
+
sleepTimerManager.setSleepTimer(SLEEP_TIMER_OFF).catch();
|
|
47
|
+
}
|
|
48
|
+
}, [playerState?.isLive]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!playerState.isReadyToPlay) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
void sleepTimerManager?.loadSleep();
|
|
56
|
+
}, [playerState.isReadyToPlay, sleepTimerManager]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const sleepSubscription =
|
|
60
|
+
sleepTimerManager?.sleepTimerState$.subscribe(setSleepTimerState);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
sleepSubscription?.unsubscribe();
|
|
64
|
+
};
|
|
65
|
+
}, [sleepTimerManager]);
|
|
66
|
+
|
|
67
|
+
const onSleepSelect = useCallback(
|
|
68
|
+
async (item) => {
|
|
69
|
+
await sleepTimerManager?.setSleepTimer(item);
|
|
70
|
+
},
|
|
71
|
+
[sleepTimerManager]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const onSleepButtonPress = useCallback(() => {
|
|
75
|
+
const timerDate = sleepTimerState.timerDate;
|
|
76
|
+
|
|
77
|
+
const timerRemaining =
|
|
78
|
+
timerDate !== 0 && +timerDate > Date.now() ? timerDate : null;
|
|
79
|
+
|
|
80
|
+
openBottomSheetModal({
|
|
81
|
+
modalBottomSheetContentProps: {
|
|
82
|
+
headerComponent: SleepTimerModalHeader,
|
|
83
|
+
items: getSleepTimerOptions(offLabel, timerRemaining),
|
|
84
|
+
onPress: onSleepSelect,
|
|
85
|
+
title,
|
|
86
|
+
summary: timerRemaining,
|
|
87
|
+
itemProps: getBottomSheetModalItemProps,
|
|
88
|
+
iconProps: getBottomSheetModalIconProps,
|
|
89
|
+
labelProps: getBottomSheetModalLabelProps,
|
|
90
|
+
getSelectedItemIcon: getBottomSheetModalSelectedItemIcon,
|
|
91
|
+
},
|
|
92
|
+
options: {
|
|
93
|
+
supportedOrientations: [
|
|
94
|
+
"portrait",
|
|
95
|
+
"landscape",
|
|
96
|
+
"portrait-upside-down",
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}, [sleepTimerState.timerDate, sleepTimerState.timer, title, onSleepSelect]);
|
|
101
|
+
|
|
102
|
+
return useMemo(
|
|
103
|
+
() => ({
|
|
104
|
+
onSleepButtonPress,
|
|
105
|
+
sleepTimerState,
|
|
106
|
+
}),
|
|
107
|
+
[onSleepButtonPress, sleepTimerState]
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
2
|
+
import {
|
|
3
|
+
PlayerAnimationStateEnum,
|
|
4
|
+
PlayerAnimationStateT,
|
|
5
|
+
} from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation/ModalAnimationContext";
|
|
2
6
|
import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
7
|
+
import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
3
8
|
|
|
4
9
|
export const NAMESPACE = "audio-player";
|
|
5
10
|
|
|
@@ -31,3 +36,25 @@ export const formatSpeed = (value: number) => {
|
|
|
31
36
|
// Return the formatted string with "speed_" prefix
|
|
32
37
|
return `speed_${formattedValue}`;
|
|
33
38
|
};
|
|
39
|
+
|
|
40
|
+
export const calculateBorderRadius = (
|
|
41
|
+
docked: boolean,
|
|
42
|
+
startComponentsAnimation: boolean,
|
|
43
|
+
isActiveGesture: boolean,
|
|
44
|
+
playerAnimationState: PlayerAnimationStateT,
|
|
45
|
+
configuration: Record<string, any>
|
|
46
|
+
): number => {
|
|
47
|
+
const shouldRemoveBorderRadius =
|
|
48
|
+
(startComponentsAnimation &&
|
|
49
|
+
isActiveGesture &&
|
|
50
|
+
playerAnimationState !== PlayerAnimationStateEnum.maximize) ||
|
|
51
|
+
docked;
|
|
52
|
+
|
|
53
|
+
if (shouldRemoveBorderRadius) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return toNumberWithDefaultZero(
|
|
58
|
+
configuration.audio_player_artwork_border_radius
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { toDayJSLocaleMap } from "@applicaster/zapp-react-native-utils/appUtils/localizationsHelper";
|
|
2
|
+
import { dayjs } from "@applicaster/zapp-react-native-utils/dateUtils";
|
|
3
|
+
import { getUILanguage } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
4
|
+
|
|
5
|
+
const createTimerOptions = (value) => {
|
|
6
|
+
let label = "";
|
|
7
|
+
|
|
8
|
+
const str = dayjs
|
|
9
|
+
.duration(value, "minute")
|
|
10
|
+
.locale(toDayJSLocaleMap(getUILanguage()))
|
|
11
|
+
.humanize();
|
|
12
|
+
|
|
13
|
+
if (value === 60) {
|
|
14
|
+
/** Skipping prefix "an"/"un"/"una" for 1 hour */
|
|
15
|
+
|
|
16
|
+
if (str.includes(" ")) {
|
|
17
|
+
label = `1 ${str.split(" ")[1]}`; // Combine "1" with second part of the string
|
|
18
|
+
} else {
|
|
19
|
+
label = `1 ${str}`; // Use dayjs provided translation
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
label = str;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
label,
|
|
27
|
+
value,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getSleepTimerOptions = (offLabel, sleepTimerValue) => {
|
|
32
|
+
return [
|
|
33
|
+
sleepTimerValue
|
|
34
|
+
? {
|
|
35
|
+
label: offLabel,
|
|
36
|
+
value: 0,
|
|
37
|
+
}
|
|
38
|
+
: null,
|
|
39
|
+
...[5, 10, 15, 30, 45, 60, 120].map(createTimerOptions),
|
|
40
|
+
];
|
|
41
|
+
};
|