@applicaster/quick-brick-player 13.0.0-rc.99 → 14.0.0-rc.2

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.
Files changed (34) hide show
  1. package/package.json +5 -6
  2. package/src/Player/AudioLayer/AudioDescription.tsx +109 -68
  3. package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +81 -189
  4. package/src/Player/AudioLayer/Layout/Controls/VerticalGutter.tsx +23 -5
  5. package/src/Player/AudioLayer/Layout/Controls/index.tsx +24 -49
  6. package/src/Player/AudioLayer/Layout/Controls/utils.ts +2 -2
  7. package/src/Player/AudioLayer/Layout/DockedControls/index.tsx +38 -19
  8. package/src/Player/AudioLayer/Layout/MobileLayout.tsx +4 -13
  9. package/src/Player/AudioLayer/Layout/PlayerImage/AnimatedImage.tsx +38 -34
  10. package/src/Player/AudioLayer/Layout/PlayerImage/FlexImage.tsx +4 -16
  11. package/src/Player/AudioLayer/Layout/PlayerImage/hooks/index.ts +1 -0
  12. package/src/Player/AudioLayer/Layout/PlayerImage/hooks/useChangePlayerState.ts +99 -0
  13. package/src/Player/AudioLayer/Layout/PlayerImage/index.tsx +37 -20
  14. package/src/Player/AudioLayer/Layout/PlayerImage/styles.ts +19 -0
  15. package/src/Player/AudioLayer/Layout/TabletLandscapeLayout.tsx +101 -105
  16. package/src/Player/AudioLayer/Layout/TabletPortraitLayout.tsx +2 -11
  17. package/src/Player/AudioLayer/Layout/TransportInfo/index.tsx +19 -5
  18. package/src/Player/AudioLayer/Layout/const.ts +4 -0
  19. package/src/Player/AudioLayer/Layout/hooks/useTransportInfoHeight.ts +1 -1
  20. package/src/Player/AudioLayer/Layout/utils.ts +7 -0
  21. package/src/Player/AudioLayer/SleepTimerManager.ts +22 -11
  22. package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +66 -0
  23. package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +1 -0
  24. package/src/Player/AudioLayer/useSleepTimerModal.ts +109 -0
  25. package/src/Player/Const/sleepTimerOptions.ts +7 -5
  26. package/src/Player/Utils/index.tsx +6 -0
  27. package/src/Player/hooks/useIsCasting.tsx +57 -0
  28. package/src/Player/index.tsx +78 -33
  29. package/src/QuickBrickPlayer.tsx +3 -0
  30. package/src/Player/AudioLayer/LinearGradient/LinearGradientProxy.ts +0 -3
  31. package/src/Player/AudioLayer/LinearGradient/LinearGradientProxy.web.ts +0 -3
  32. package/src/Player/AudioLayer/LinearGradient/index.ts +0 -1
  33. package/src/Player/AudioLayer/MagicBG/index.tsx +0 -155
  34. package/src/utils/utils.ts +0 -25
@@ -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
+ };
@@ -28,12 +28,14 @@ const createTimerOptions = (value) => {
28
28
  };
29
29
  };
30
30
 
31
- export const getSleepTimerOptions = (configuration) => {
31
+ export const getSleepTimerOptions = (offLabel, sleepTimerValue) => {
32
32
  return [
33
- {
34
- label: configuration["playback_sleep_off_label"],
35
- value: 0,
36
- },
33
+ sleepTimerValue
34
+ ? {
35
+ label: offLabel,
36
+ value: 0,
37
+ }
38
+ : null,
37
39
  ...[5, 10, 15, 30, 45, 60, 120].map(createTimerOptions),
38
40
  ];
39
41
  };
@@ -116,3 +116,9 @@ export const getBottomSheetModalSelectedItemIcon = (theme) =>
116
116
 
117
117
  export const getBottomSheetModalDefaultItemIcon = (theme) =>
118
118
  theme["audio_modal_bottom_sheet_item_default_icon"] || null;
119
+
120
+ export const selectSleepTitle = (configuration) =>
121
+ configuration.playback_sleep_title;
122
+
123
+ export const selectSleepOffLabel = (configuration) =>
124
+ configuration.playback_sleep_off_label;
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
3
+ import { useAppState } from "@applicaster/zapp-react-native-utils/reactHooks/app";
4
+ import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
5
+
6
+ const CHROMECAST_IDENTIFIER = "quick-brick-chromecast-action";
7
+
8
+ const fakeEntry: any = {
9
+ title: "Chromecast use is casting fake entry",
10
+ id: "quick-brick-use-is-casting-fake-entry",
11
+ };
12
+
13
+ /**
14
+ * Returns the cast action state connection status, does not update when app is in background on Apple platforms
15
+ * On other platforms, it continues to check the cast action state even when the app is in background
16
+ */
17
+ export const useIsCasting = () => {
18
+ const actionContext = useActions(CHROMECAST_IDENTIFIER);
19
+ const appState = useAppState();
20
+ const isAppInForeground = appState === "active";
21
+ const shouldOnlyUpdateInForeground = isApplePlatform();
22
+
23
+ const [castActionState, setCastActionState] = React.useState(
24
+ !actionContext ? null : actionContext.initialEntryState(fakeEntry)
25
+ );
26
+
27
+ React.useEffect(() => {
28
+ if (!shouldOnlyUpdateInForeground || isAppInForeground) {
29
+ if (actionContext && castActionState === null) {
30
+ setCastActionState(actionContext.initialEntryState(fakeEntry));
31
+ }
32
+ }
33
+ }, [
34
+ actionContext,
35
+ setCastActionState,
36
+ isAppInForeground,
37
+ shouldOnlyUpdateInForeground,
38
+ ]);
39
+
40
+ React.useEffect(() => {
41
+ if (typeof actionContext?.addListener === "function") {
42
+ return actionContext?.addListener(String(fakeEntry.id), (state) => {
43
+ // Only check foreground state on Apple platforms
44
+ if (!shouldOnlyUpdateInForeground || isAppInForeground) {
45
+ setCastActionState(state);
46
+ }
47
+ });
48
+ }
49
+ }, [
50
+ setCastActionState,
51
+ actionContext,
52
+ isAppInForeground,
53
+ shouldOnlyUpdateInForeground,
54
+ ]);
55
+
56
+ return !!castActionState?.connected;
57
+ };
@@ -4,6 +4,8 @@
4
4
  import React from "react";
5
5
  import * as R from "ramda";
6
6
 
7
+ import { flatten } from "@applicaster/zapp-react-native-utils/utils";
8
+
7
9
  import { findNodeHandle, StyleSheet, View } from "react-native";
8
10
  import TransportControlsMobile from "@applicaster/quick-brick-mobile-transport-controls";
9
11
  import TransportControlsTV from "@applicaster/quick-brick-tv-transport-controls";
@@ -40,6 +42,7 @@ import { PlayerAnalyticsManager } from "@applicaster/zapp-react-native-utils/ana
40
42
  import { PlayerAnalyticsTrackerI } from "@applicaster/zapp-react-native-utils/analyticsUtils/playerAnalyticsTracker";
41
43
 
42
44
  import { RNPlayerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/nativePlayerManager";
45
+ import { isAudioItem as isAudioItemUtil } from "@applicaster/zapp-react-native-utils/playerUtils";
43
46
 
44
47
  const styles = StyleSheet.create({
45
48
  container: { flex: 1 },
@@ -50,6 +53,7 @@ const styles = StyleSheet.create({
50
53
  position: "absolute",
51
54
  zIndex: 1,
52
55
  },
56
+ audioPlayerSize: { width: "100%", height: "100%" },
53
57
  playerSize: { width: "100%", height: "100%" },
54
58
  });
55
59
 
@@ -58,6 +62,10 @@ const logger = createLogger({
58
62
  subsystem: Subsystem,
59
63
  });
60
64
 
65
+ const isTvOS = isTvOSPlatform();
66
+
67
+ const AUDIO_PLAYER_NATIVE_ID = "audio-player-view";
68
+
61
69
  export default class VideoPlayer extends React.Component<
62
70
  QuickBrickPlayer.PlayerProps,
63
71
  QuickBrickPlayer.NativePlayerState
@@ -74,6 +82,7 @@ export default class VideoPlayer extends React.Component<
74
82
  state: QuickBrickPlayer.NativePlayerState = {
75
83
  audioTrackId: null,
76
84
  textTrackId: null,
85
+ isCasting: this.props.isCasting,
77
86
 
78
87
  // TODO: Can be deleted, on controller logic
79
88
  isAd: false,
@@ -92,6 +101,8 @@ export default class VideoPlayer extends React.Component<
92
101
  // Player state
93
102
  currentTime: 0,
94
103
  duration: 0,
104
+
105
+ nativePlayerIsReady: false,
95
106
  };
96
107
 
97
108
  private _tracker: PlayerAnalyticsTrackerI;
@@ -487,6 +498,9 @@ export default class VideoPlayer extends React.Component<
487
498
 
488
499
  onVideoFullscreenPlayerDidPresent = () => {
489
500
  this.props.listener?.onVideoFullscreenPlayerDidPresent?.();
501
+
502
+ // we have to have a signal that player is ready, in order to show audio_player(for example)
503
+ this.setState({ nativePlayerIsReady: true });
490
504
  };
491
505
 
492
506
  onVideoFullscreenPlayerWillDismiss = () => {
@@ -672,32 +686,57 @@ export default class VideoPlayer extends React.Component<
672
686
  }
673
687
  };
674
688
 
675
- childrenWithPlayNext = () => {
676
- if (!isTvOSPlatform() || !this.getPlayNextData()) {
677
- return this.props.children;
689
+ childrenWithExtensions = () => {
690
+ const { docked, entry, children } = this.props;
691
+
692
+ if (!isTvOS) {
693
+ return children;
678
694
  }
679
695
 
680
- // tvos only
681
- const { docked, entry } = this.props;
682
-
683
- const children = R.pathOr([], "children", this.props);
684
-
685
- return [
686
- ...children,
687
- <PlayNextOverlay
688
- key="play-next-overlay"
689
- item={entry}
690
- playerIsDocked={docked}
691
- pluginConfiguration={this.getPluginConfiguration()}
692
- playerId={this.props.playerId}
693
- playNextData={this.getPlayNextData()}
694
- setNextVideoPreloadThresholdPercentage={
695
- this.props.setNextVideoPreloadThresholdPercentage
696
- }
697
- />,
698
- ];
696
+ const output = [];
697
+
698
+ // audio-player
699
+ if (isAudioItemUtil(entry)) {
700
+ const { nativePlayerIsReady } = this.state;
701
+
702
+ // we have to wait ready status of native player view, before showing audio_player
703
+ if (nativePlayerIsReady) {
704
+ output.push(
705
+ <View nativeID={AUDIO_PLAYER_NATIVE_ID} key={AUDIO_PLAYER_NATIVE_ID}>
706
+ {children}
707
+ </View>
708
+ );
709
+ }
710
+ } else {
711
+ output.push(children);
712
+ }
713
+ // audio-player
714
+
715
+ // play-next
716
+ if (this.getPlayNextData()) {
717
+ output.push(
718
+ <PlayNextOverlay
719
+ key="play-next-overlay"
720
+ item={entry}
721
+ playerIsDocked={docked}
722
+ pluginConfiguration={this.getPluginConfiguration()}
723
+ playerId={this.props.playerId}
724
+ playNextData={this.getPlayNextData()}
725
+ setNextVideoPreloadThresholdPercentage={
726
+ this.props.setNextVideoPreloadThresholdPercentage
727
+ }
728
+ />
729
+ );
730
+ }
731
+ // play-next
732
+
733
+ return flatten(output); // prevent possible case with nested array
699
734
  };
700
735
 
736
+ get supportNativeCast() {
737
+ return this.props.controller.supportNativeCast();
738
+ }
739
+
701
740
  render() {
702
741
  const {
703
742
  docked,
@@ -710,18 +749,13 @@ export default class VideoPlayer extends React.Component<
710
749
  muted,
711
750
  style,
712
751
  castAction,
713
- controller,
714
752
  startComponentsAnimation,
715
753
  } = this.props;
716
754
 
717
- const supportNativeCast = controller.supportNativeCast();
718
-
719
755
  const { audioTrackId, keyEvent, paused, seek, textTrackId, rate } =
720
756
  this.state;
721
757
 
722
- const isAudioItem =
723
- entry?.content?.type?.includes?.("audio") ||
724
- entry?.type?.value?.includes?.("audio");
758
+ const isAudioItem = isAudioItemUtil(entry);
725
759
 
726
760
  const getStyle = (dimensions) => {
727
761
  const generalStyles = isAudioItem ? styles.audio : styles.video;
@@ -995,11 +1029,14 @@ export default class VideoPlayer extends React.Component<
995
1029
  const backgroundImageKey =
996
1030
  this.getPluginConfiguration()?.player_preview_image_key;
997
1031
 
1032
+ const shouldRenderNativePlayer =
1033
+ !this.props.isCasting || this.supportNativeCast;
1034
+
998
1035
  const renderMobilePlayer = () => {
999
1036
  return isFullScreenAudioPlayer && isAudioItem ? (
1000
1037
  // Ensure the View remains in the native hierarchy for proper rendering and interaction
1001
1038
  <View collapsable={false} style={containerStyle}>
1002
- {!castAction?.state?.connected || supportNativeCast ? (
1039
+ {shouldRenderNativePlayer ? (
1003
1040
  <this.props.PlayerComponent
1004
1041
  ref={this._assignRoot}
1005
1042
  {...nativeEvents}
@@ -1051,7 +1088,7 @@ export default class VideoPlayer extends React.Component<
1051
1088
  />
1052
1089
  ) : null}
1053
1090
 
1054
- {!castAction?.state?.connected || supportNativeCast ? (
1091
+ {shouldRenderNativePlayer ? (
1055
1092
  <AnimationComponent
1056
1093
  animationType={ComponentAnimationType.player}
1057
1094
  additionalData={{
@@ -1073,7 +1110,7 @@ export default class VideoPlayer extends React.Component<
1073
1110
  pluginConfiguration={this.getPluginConfiguration()}
1074
1111
  controller={this.props.controller}
1075
1112
  >
1076
- {this.childrenWithPlayNext()}
1113
+ {this.props.children}
1077
1114
  </this.props.PlayerComponent>
1078
1115
  </AnimationComponent>
1079
1116
  ) : null}
@@ -1102,6 +1139,14 @@ export default class VideoPlayer extends React.Component<
1102
1139
  const renderTVPlayer = () => {
1103
1140
  return (
1104
1141
  <View style={this.takeParentStyle} testID={"player-view"}>
1142
+ {!isTvOS && isAudioItem ? (
1143
+ <AudioPlayer
1144
+ style={getStyle(styles.audioPlayerSize)}
1145
+ audio_item={entry}
1146
+ plugin_configuration={this.getPluginConfiguration()}
1147
+ />
1148
+ ) : null}
1149
+
1105
1150
  <this.props.PlayerComponent
1106
1151
  ref={this._assignRoot}
1107
1152
  {...nativeEvents}
@@ -1111,11 +1156,11 @@ export default class VideoPlayer extends React.Component<
1111
1156
  pluginConfiguration={this.getPluginConfiguration()}
1112
1157
  controller={this.props.controller}
1113
1158
  >
1114
- {this.childrenWithPlayNext()}
1159
+ {this.childrenWithExtensions()}
1115
1160
  </this.props.PlayerComponent>
1116
1161
 
1117
1162
  {renderTransportControls(this.takeParentStyle)}
1118
- {!isTvOSPlatform() && (
1163
+ {!isTvOS && (
1119
1164
  <PlayNextOverlay
1120
1165
  item={entry}
1121
1166
  playerIsDocked={docked}
@@ -5,6 +5,7 @@ import Player from "./Player";
5
5
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
6
6
  import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
7
7
  import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
8
+ import { useIsCasting } from "./Player/hooks/useIsCasting";
8
9
 
9
10
  const CHROMECAST_IDENTIFIER = "quick-brick-chromecast-action";
10
11
 
@@ -24,6 +25,7 @@ const QuickBrickPlayerComponent = (
24
25
  }, [props.PlayerComponent]);
25
26
 
26
27
  const castAction = useActions(CHROMECAST_IDENTIFIER);
28
+ const isCasting = useIsCasting();
27
29
 
28
30
  const isModal = Boolean(props?.isModal);
29
31
  const isScreenDisabled = !controller?.isFullScreenSupported() || !isModal;
@@ -48,6 +50,7 @@ const QuickBrickPlayerComponent = (
48
50
  {...props}
49
51
  ref={ref}
50
52
  castAction={!props?.disableCastAction && castAction}
53
+ isCasting={isCasting}
51
54
  PlayerComponent={props.PlayerComponent}
52
55
  openPiP={openPiP}
53
56
  closePiP={closePiP}
@@ -1,3 +0,0 @@
1
- import LinearGradient from "react-native-linear-gradient";
2
-
3
- export default LinearGradient;
@@ -1,3 +0,0 @@
1
- const WebLinearGradient = require("react-native-web-linear-gradient").default;
2
-
3
- export default WebLinearGradient;
@@ -1 +0,0 @@
1
- export { default } from "./LinearGradientProxy";
@@ -1,155 +0,0 @@
1
- /* eslint-disable react/prop-types */
2
- import * as React from "react";
3
- import { Animated, StyleSheet, View } from "react-native";
4
- import LinearGradient from "../LinearGradient";
5
- import { getDominantColor, getPalettes } from "../../../utils/utils";
6
- import { useEffect } from "react";
7
-
8
- type MagicBGProps = {
9
- children: React.ReactNode;
10
- image: string;
11
- fallbackUrl?: string;
12
- defaultColor: string;
13
- colors: string[];
14
- };
15
-
16
- const defaultColors = {
17
- overlay: "rgba(0, 0, 0, 0.35)",
18
- defaultGradientStart: "rgba(0, 0, 0, 0.25)",
19
- defaultGradientEnd: "rgba(0, 0, 0, 0)",
20
- };
21
-
22
- const styles = StyleSheet.create({
23
- container: {
24
- flex: 1,
25
- },
26
- fullScreenOverlay: {
27
- ...StyleSheet.absoluteFillObject,
28
- backgroundColor: defaultColors.overlay,
29
- },
30
- gradientOverlay: {
31
- position: "absolute",
32
- bottom: 0,
33
- left: 0,
34
- right: 0,
35
- height: "100%",
36
- },
37
- });
38
-
39
- const fetchPaletteAndDominantColor = async (imageUrl: string) => {
40
- const palette = await getPalettes(imageUrl);
41
-
42
- const colors = palette.map(
43
- (color: { r: number; g: number; b: number }) =>
44
- `rgb(${color.r}, ${color.g}, ${color.b})`
45
- );
46
-
47
- const dominant = await getDominantColor(imageUrl);
48
- const dominantColor = `rgb(${dominant.r}, ${dominant.g}, ${dominant.b})`;
49
-
50
- return { gradientColors: colors.slice(0, 2).reverse(), dominantColor };
51
- };
52
-
53
- export const MagicBG: React.FC<MagicBGProps> = ({
54
- children,
55
- image,
56
- fallbackUrl = "",
57
- colors = [],
58
- defaultColor,
59
- }) => {
60
- const [dominantColor, setDominantColor] =
61
- React.useState<string>(defaultColor);
62
-
63
- const [gradientColors, setGradientColors] = React.useState<string[]>([
64
- defaultColors.defaultGradientStart,
65
- defaultColors.defaultGradientEnd,
66
- ]);
67
-
68
- const [backgroundColorAnim] = React.useState(new Animated.Value(0)); // from 0 to 1
69
- const [gradientOpacityAnim] = React.useState(new Animated.Value(0)); // from 0 to 1
70
-
71
- useEffect(() => {
72
- const animateColors = () => {
73
- Animated.timing(backgroundColorAnim, {
74
- toValue: 1,
75
- duration: 500,
76
- useNativeDriver: false,
77
- }).start();
78
-
79
- Animated.timing(gradientOpacityAnim, {
80
- toValue: 1,
81
- duration: 500,
82
- useNativeDriver: false,
83
- }).start();
84
- };
85
-
86
- const fetchColors = async () => {
87
- if (colors && colors.length > 0) {
88
- setGradientColors(colors.slice(0, 2));
89
- setDominantColor(colors[0]);
90
- animateColors();
91
- } else if (image) {
92
- try {
93
- const { gradientColors, dominantColor } =
94
- await fetchPaletteAndDominantColor(image);
95
-
96
- setGradientColors(gradientColors);
97
- setDominantColor(dominantColor);
98
- animateColors();
99
- } catch (error) {
100
- try {
101
- const { gradientColors, dominantColor } =
102
- await fetchPaletteAndDominantColor(fallbackUrl);
103
-
104
- setGradientColors(gradientColors);
105
- setDominantColor(dominantColor);
106
- animateColors();
107
- } catch {
108
- setGradientColors([
109
- defaultColors.defaultGradientStart,
110
- defaultColors.defaultGradientEnd,
111
- ]);
112
-
113
- setDominantColor(defaultColor);
114
- }
115
- }
116
- } else {
117
- setGradientColors([
118
- defaultColors.defaultGradientStart,
119
- defaultColors.defaultGradientEnd,
120
- ]);
121
-
122
- setDominantColor(defaultColor);
123
- }
124
- };
125
-
126
- fetchColors();
127
- }, []);
128
-
129
- const interpolatedBackgroundColor = backgroundColorAnim.interpolate({
130
- inputRange: [0, 1],
131
- outputRange: ["black", dominantColor],
132
- });
133
-
134
- return (
135
- <Animated.View
136
- style={[
137
- styles.container,
138
- { backgroundColor: interpolatedBackgroundColor },
139
- ]}
140
- >
141
- <View style={styles.fullScreenOverlay} />
142
-
143
- <Animated.View
144
- style={[styles.gradientOverlay, { opacity: gradientOpacityAnim }]}
145
- >
146
- <LinearGradient
147
- colors={gradientColors}
148
- style={StyleSheet.absoluteFill}
149
- />
150
- </Animated.View>
151
-
152
- {children}
153
- </Animated.View>
154
- );
155
- };