last fixes, home and videos
|
Before Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 702 KiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 702 KiB After Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 776 KiB After Width: | Height: | Size: 703 KiB |
@@ -219,8 +219,6 @@ export const ai = {
|
||||
malik,
|
||||
bena,
|
||||
john,
|
||||
leftIcon: require("./UI/leftIcon.png"),
|
||||
rightIcon: require("./UI/rightIcon.png"),
|
||||
};
|
||||
|
||||
export const videos = {
|
||||
|
||||
|
Before Width: | Height: | Size: 795 KiB |
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import CoinIcon from "./CoinIcon";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const defaultFormatOptions = {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
};
|
||||
|
||||
const formatAmount = (value, options = defaultFormatOptions) => {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
...defaultFormatOptions,
|
||||
...options,
|
||||
}).format(value);
|
||||
} catch (_error) {
|
||||
return `${value}`;
|
||||
}
|
||||
};
|
||||
|
||||
const extractNumericValue = (value) => {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const CreditAmount = ({
|
||||
value,
|
||||
style,
|
||||
textStyle,
|
||||
iconSize = 18,
|
||||
iconStyle,
|
||||
iconPosition = "right",
|
||||
gap = 6,
|
||||
showPlus = false,
|
||||
formatterOptions,
|
||||
accessibilityLabel,
|
||||
}) => {
|
||||
const numericValue = React.useMemo(
|
||||
() => extractNumericValue(value),
|
||||
[value],
|
||||
);
|
||||
|
||||
const resolvedValue =
|
||||
numericValue !== null
|
||||
? numericValue
|
||||
: value ?? 0;
|
||||
|
||||
const formattedValue =
|
||||
numericValue !== null
|
||||
? formatAmount(resolvedValue, formatterOptions) ?? `${resolvedValue}`
|
||||
: typeof resolvedValue === "string"
|
||||
? resolvedValue
|
||||
: `${resolvedValue}`;
|
||||
|
||||
const prefix =
|
||||
numericValue !== null && showPlus && numericValue > 0 ? "+" : "";
|
||||
|
||||
const a11yLabel =
|
||||
accessibilityLabel || `${prefix}${formattedValue} pièces`;
|
||||
|
||||
const containerStyles = Array.isArray(style)
|
||||
? [styles.container, { gap }, ...style]
|
||||
: [styles.container, { gap }, style];
|
||||
|
||||
const textStyles = Array.isArray(textStyle)
|
||||
? [styles.value, ...textStyle]
|
||||
: [styles.value, textStyle];
|
||||
|
||||
const iconStyles = Array.isArray(iconStyle)
|
||||
? [styles.icon, ...iconStyle]
|
||||
: [styles.icon, iconStyle];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={containerStyles}
|
||||
accessibilityRole="text"
|
||||
accessibilityLabel={a11yLabel}
|
||||
>
|
||||
{iconPosition === "left" ? (
|
||||
<CoinIcon size={iconSize} style={iconStyles} />
|
||||
) : null}
|
||||
<Text style={textStyles}>{`${prefix}${formattedValue}`}</Text>
|
||||
{iconPosition === "right" ? (
|
||||
<CoinIcon size={iconSize} style={iconStyles} />
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
value: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 16,
|
||||
color: "#fff",
|
||||
},
|
||||
icon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
},
|
||||
});
|
||||
|
||||
export default CreditAmount;
|
||||
@@ -11,6 +11,7 @@ const MusicLandHeader = ({
|
||||
showSkip = false,
|
||||
progress = 1,
|
||||
logo = null,
|
||||
style,
|
||||
}) => {
|
||||
const isWeb = Platform.OS === "web";
|
||||
const backButtonStyle = isWeb
|
||||
@@ -34,7 +35,7 @@ const MusicLandHeader = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: "center", gap: 16 }}>
|
||||
<View style={{ alignItems: "center", gap: 16, ...style }}>
|
||||
<View style={{ width: "100%", ...Style.containerRow, gap: 16 }}>
|
||||
<Pressable style={backButtonStyle} onPress={onPressBack}>
|
||||
<Image
|
||||
@@ -72,14 +73,16 @@ const MusicLandHeader = ({
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
<Image
|
||||
source={logo}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: isWeb ? 150 : 100,
|
||||
resizeMode: "contain",
|
||||
}}
|
||||
/>
|
||||
{logo && (
|
||||
<Image
|
||||
source={logo}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: isWeb ? 150 : 100,
|
||||
resizeMode: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
@@ -24,6 +24,7 @@ export default ({
|
||||
const offset = useSharedValue(0);
|
||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
||||
const [layout, setLayout] = useState(null);
|
||||
const seekingRef = useRef(false);
|
||||
|
||||
const SLIDER_WIDTH = layout?.width;
|
||||
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;
|
||||
@@ -31,12 +32,34 @@ export default ({
|
||||
const pan = Gesture.Pan()
|
||||
.enabled(seekEnabled)
|
||||
.onBegin(() => {
|
||||
if (seekEnabled && typeof onSeekStart === "function") {
|
||||
// Notify JS thread that user started seeking (e.g., pause audio)
|
||||
if (
|
||||
seekEnabled &&
|
||||
typeof onSeekStart === "function" &&
|
||||
!seekingRef.current
|
||||
) {
|
||||
seekingRef.current = true;
|
||||
runOnJS(onSeekStart)();
|
||||
}
|
||||
})
|
||||
.onStart(() => {
|
||||
if (
|
||||
seekEnabled &&
|
||||
typeof onSeekStart === "function" &&
|
||||
!seekingRef.current
|
||||
) {
|
||||
seekingRef.current = true;
|
||||
runOnJS(onSeekStart)();
|
||||
}
|
||||
})
|
||||
.onChange((event) => {
|
||||
if (
|
||||
seekEnabled &&
|
||||
typeof onSeekStart === "function" &&
|
||||
!seekingRef.current
|
||||
) {
|
||||
seekingRef.current = true;
|
||||
runOnJS(onSeekStart)();
|
||||
}
|
||||
offset.value =
|
||||
Math.abs(offset.value) <= MAX_VALUE
|
||||
? offset.value + event.changeX <= 0
|
||||
@@ -50,14 +73,22 @@ export default ({
|
||||
boxWidth.value = newWidth;
|
||||
})
|
||||
.onEnd(() => {
|
||||
if (!seekEnabled || !onSeek || !MAX_VALUE) return;
|
||||
const ratio =
|
||||
MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
|
||||
// Reanimated -> JS thread bridge
|
||||
runOnJS(onSeek)(ratio);
|
||||
if (seekEnabled && typeof onSeek === "function" && MAX_VALUE) {
|
||||
const ratio =
|
||||
MAX_VALUE > 0
|
||||
? Math.min(1, Math.max(0, offset.value / MAX_VALUE))
|
||||
: 0;
|
||||
// Reanimated -> JS thread bridge
|
||||
runOnJS(onSeek)(ratio);
|
||||
}
|
||||
if (seekingRef.current && typeof onSeekEnd === "function") {
|
||||
seekingRef.current = false;
|
||||
runOnJS(onSeekEnd)();
|
||||
}
|
||||
})
|
||||
.onFinalize(() => {
|
||||
if (seekEnabled && typeof onSeekEnd === "function") {
|
||||
if (seekingRef.current && typeof onSeekEnd === "function") {
|
||||
seekingRef.current = false;
|
||||
runOnJS(onSeekEnd)();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
Linking,
|
||||
Modal,
|
||||
Pressable,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { icons } from "../../assets";
|
||||
import BorderGradientButton from "../BorderGradientButton";
|
||||
import GradientButton from "../GradientButton";
|
||||
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
|
||||
@@ -19,6 +17,7 @@ import { Palette, gutters } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { getFunctionsClient } from "../../config/firebase";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import CreditAmount from "../CreditAmount";
|
||||
|
||||
const WEB_MODAL_MAX_WIDTH = 1000;
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
@@ -73,10 +72,12 @@ function CoinPackCard({ pack, selected, onSelect }) {
|
||||
>
|
||||
<BlurView intensity={20} tint="dark" style={styles.cardBlur}>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.coinRow}>
|
||||
<Image source={icons.coin} style={styles.coinIcon} />
|
||||
<Text style={styles.coinAmount}>{pack?.coinAmount} pièces</Text>
|
||||
</View>
|
||||
<CreditAmount
|
||||
value={pack?.coinAmount}
|
||||
style={styles.coinRow}
|
||||
textStyle={styles.coinAmount}
|
||||
iconSize={26}
|
||||
/>
|
||||
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
|
||||
</View>
|
||||
{pack?.description ? (
|
||||
@@ -395,11 +396,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
coinIcon: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
resizeMode: "contain",
|
||||
},
|
||||
coinAmount: {
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
fontSize: 20,
|
||||
|
||||
@@ -4,9 +4,9 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import React from "reactn";
|
||||
import { responsiveHeight } from "../actions/responsiveSizes.js";
|
||||
import { icons, subBadges } from "../assets";
|
||||
import { subBadges } from "../assets";
|
||||
import BaseHeader from "../components/BaseHeader";
|
||||
import CoinIcon from "../components/CoinIcon";
|
||||
import CreditAmount from "../components/CreditAmount";
|
||||
import ConnectBtn from "../components/ConnectBtn.js";
|
||||
import CoinPackModal from "../components/modal/CoinPackModal";
|
||||
import NavigateHeader from "../components/NavigateHeader";
|
||||
@@ -65,16 +65,6 @@ export default ({
|
||||
return 0;
|
||||
}, [currentUserData?.coins]);
|
||||
|
||||
const formattedCoins = React.useMemo(() => {
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(coinBalance);
|
||||
} catch (_error) {
|
||||
return `${coinBalance}`;
|
||||
}
|
||||
}, [coinBalance]);
|
||||
|
||||
const showCoinBadge = isWeb && !!currentUID && showCoin;
|
||||
const [isCoinModalVisible, setCoinModalVisible] = React.useState(false);
|
||||
|
||||
@@ -184,7 +174,6 @@ export default ({
|
||||
flex: 1,
|
||||
minHeight: isWeb ? "100svh" : "100%",
|
||||
position: "relative",
|
||||
// paddingHorizontal: isWeb ? gutters : 0,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
>
|
||||
@@ -192,7 +181,6 @@ export default ({
|
||||
<Image
|
||||
source={backgroundImg}
|
||||
resizeMode="cover"
|
||||
// blurRadius={!isWeb ? blurIntensity : undefined}
|
||||
blurIntensity={blurIntensity}
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -233,16 +221,15 @@ export default ({
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<CoinIcon size={22} />
|
||||
<Text
|
||||
style={{
|
||||
<CreditAmount
|
||||
value={coinBalance}
|
||||
iconSize={22}
|
||||
textStyle={{
|
||||
color: "#ffffff",
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{formattedCoins}
|
||||
</Text>
|
||||
/>
|
||||
</Pressable>
|
||||
{subscriptionBadgeSource ? (
|
||||
<Pressable
|
||||
|
||||
@@ -62,7 +62,7 @@ export const BottomTabScreen = () => {
|
||||
name={Routes.Playbacks}
|
||||
component={Playbacks}
|
||||
options={{
|
||||
tabBarLabel: "Playbacks",
|
||||
tabBarLabel: "Playback",
|
||||
headerShown: false,
|
||||
tabBarShowLabel: true,
|
||||
tabBarIcon: ({ focused }) => renderIcon(tabs.mic, focused),
|
||||
|
||||
@@ -64,7 +64,7 @@ export const Routes = {
|
||||
|
||||
Create: "Create",
|
||||
HitParade: "HitParade",
|
||||
Playbacks: "Playbacks",
|
||||
Playbacks: "Playback",
|
||||
|
||||
Library: "Library",
|
||||
AllMyPlaylist: "AllMyPlaylist",
|
||||
|
||||
@@ -12,10 +12,12 @@ import firebase, {
|
||||
playlistsRef,
|
||||
projectsRef,
|
||||
usersRef,
|
||||
videosRef,
|
||||
} from "../config/firebase";
|
||||
import { getUserPreferredArtistName } from "../utils/artistName";
|
||||
import { getLikeFieldPath, LIKE_TARGET } from "../utils/likes";
|
||||
import { ensureAuthenticated } from "../utils/authRedirect";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
|
||||
const SONG_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.SONG);
|
||||
const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK);
|
||||
@@ -43,20 +45,18 @@ export default ({ children }) => {
|
||||
});
|
||||
|
||||
// Subscribe to user's projects (musics)
|
||||
const {
|
||||
data: userProjects = [],
|
||||
loading: userProjectsLoading = true,
|
||||
} = useDataFromRef({
|
||||
ref: currentUID
|
||||
? projectsRef
|
||||
.where("userId", "==", currentUID)
|
||||
.orderBy("updatedAt", "desc")
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
const { data: userProjects = [], loading: userProjectsLoading = true } =
|
||||
useDataFromRef({
|
||||
ref: currentUID
|
||||
? projectsRef
|
||||
.where("userId", "==", currentUID)
|
||||
.orderBy("updatedAt", "desc")
|
||||
: null,
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: !!currentUID,
|
||||
refreshArray: [currentUID],
|
||||
});
|
||||
|
||||
const userPlaybacks = Array.isArray(userProjects)
|
||||
? userProjects.filter((project) => project.playbackUrl)
|
||||
@@ -136,17 +136,17 @@ export default ({ children }) => {
|
||||
{
|
||||
selectedProjectId: projectId || null,
|
||||
},
|
||||
{ merge: true }
|
||||
{ merge: true },
|
||||
);
|
||||
console.log("update selected project");
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"UserDataProvider: unable to persist selectedProjectId",
|
||||
error?.message || error
|
||||
error?.message || error,
|
||||
);
|
||||
}
|
||||
},
|
||||
[currentUID]
|
||||
[currentUID],
|
||||
);
|
||||
|
||||
const selectProject = useCallback(
|
||||
@@ -161,12 +161,12 @@ export default ({ children }) => {
|
||||
persistSelectedProjectId(safeId).catch((error) => {
|
||||
console.log(
|
||||
"UserDataProvider: persist selectedProjectId failed",
|
||||
error?.message || error
|
||||
error?.message || error,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
[currentUID, persistSelectedProjectId, setSelectedProject]
|
||||
[currentUID, persistSelectedProjectId, setSelectedProject],
|
||||
);
|
||||
|
||||
const resetSelectedProject = useCallback(() => {
|
||||
@@ -183,7 +183,7 @@ export default ({ children }) => {
|
||||
...partial,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
options
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("updateProjectData error", e?.message);
|
||||
@@ -209,7 +209,7 @@ export default ({ children }) => {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
const artistDisplayName = getUserPreferredArtistName(
|
||||
currentUserDoc || currentUserData || {}
|
||||
currentUserDoc || currentUserData || {},
|
||||
);
|
||||
const payload = {
|
||||
userId: currentUID || null,
|
||||
@@ -238,7 +238,7 @@ export default ({ children }) => {
|
||||
followedBy: arrayUnion(currentUID),
|
||||
lastFollowersUpdateAt: new Date(),
|
||||
},
|
||||
{ merge: true }
|
||||
{ merge: true },
|
||||
);
|
||||
setTooltip({ type: "success", text: "Abonnement mis à jour" });
|
||||
}
|
||||
@@ -259,7 +259,7 @@ export default ({ children }) => {
|
||||
followedBy: arrayRemove(currentUID),
|
||||
lastFollowersUpdateAt: new Date(),
|
||||
},
|
||||
{ merge: true }
|
||||
{ merge: true },
|
||||
);
|
||||
setTooltip({
|
||||
type: "success",
|
||||
@@ -416,6 +416,11 @@ export default ({ children }) => {
|
||||
userProjects,
|
||||
]);
|
||||
|
||||
const { data: videos } = useDataFromRef({
|
||||
ref: videosRef.doc("fr"),
|
||||
simpleRef: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<UserDataContext.Provider
|
||||
value={{
|
||||
@@ -452,6 +457,8 @@ export default ({ children }) => {
|
||||
setSelectedProjectId: selectProject,
|
||||
updateProjectData,
|
||||
createNewProject,
|
||||
|
||||
videos,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -274,7 +274,7 @@ const HitParade = () => {
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{["Chansons", "Playbacks"].map((item, index) => (
|
||||
{["Chansons", "Playback"].map((item, index) => (
|
||||
<BorderGradientButton
|
||||
key={index}
|
||||
title={item}
|
||||
@@ -291,7 +291,7 @@ const HitParade = () => {
|
||||
|
||||
{selected === "Chansons" &&
|
||||
(isWeb ? renderSongsWeb() : renderSongsMobile())}
|
||||
{selected === "Playbacks" &&
|
||||
{selected === "Playback" &&
|
||||
(isWeb ? renderPlaybacksWeb() : renderPlaybacksMobile())}
|
||||
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
|
||||
</View>
|
||||
|
||||
@@ -8,15 +8,14 @@ import React, {
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { background, cardsImg, icons, videos } from "../../assets";
|
||||
import { background, cardsImg, icons } from "../../assets";
|
||||
import Alert from "../../components/Alert";
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MoreMenu from "../../components/MoreMenu";
|
||||
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
|
||||
import ShareBtn from "../../components/ShareBtn/ShareBtn";
|
||||
import { projectsRef, usersRef, videosRef } from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { projectsRef, usersRef } from "../../config/firebase";
|
||||
import { isWeb } from "../../hooks/useLayoutType.js";
|
||||
import Page from "../../layouts/Page";
|
||||
import LandingPage from "../LandingPage";
|
||||
@@ -105,6 +104,7 @@ const Home = ({ navigation, route }) => {
|
||||
currentUserData,
|
||||
currentUID,
|
||||
createNewProject,
|
||||
videos,
|
||||
} = useUser();
|
||||
const { setTooltip } = useMinuit();
|
||||
|
||||
@@ -124,9 +124,7 @@ const Home = ({ navigation, route }) => {
|
||||
const statusCandidates = [
|
||||
pickStatus(currentUserData?.stripeSubscriptionStatus),
|
||||
pickStatus(currentUserData?.stripeSubscription?.status),
|
||||
pickStatus(
|
||||
currentUserData?.stripeSubscription?.stripeSubscriptionStatus,
|
||||
),
|
||||
pickStatus(currentUserData?.stripeSubscription?.stripeSubscriptionStatus),
|
||||
pickStatus(currentUserData?.stripeSubscription?.metadata?.status),
|
||||
].filter(Boolean);
|
||||
|
||||
@@ -410,11 +408,7 @@ const Home = ({ navigation, route }) => {
|
||||
const adventureStarted =
|
||||
hasLocalAdventureFlag || !!currentUserData?.adventureStarted;
|
||||
|
||||
const { data: video } = useDataFromRef({
|
||||
ref: videosRef.doc("fr"),
|
||||
simpleRef: true,
|
||||
});
|
||||
const videoUrl = isWeb ? video?.landingWeb : video?.landing;
|
||||
const videoUrl = isWeb ? videos?.landingWeb : videos?.landing;
|
||||
const homeBackgroundImage = background.bgTrans;
|
||||
const landingBackgroundImage = homeBackgroundImage;
|
||||
|
||||
@@ -447,12 +441,7 @@ const Home = ({ navigation, route }) => {
|
||||
}
|
||||
navigate(action.route, action.params);
|
||||
},
|
||||
[
|
||||
currentProject,
|
||||
ensureProjectSelected,
|
||||
hasActiveProject,
|
||||
handleStartNew,
|
||||
],
|
||||
[currentProject, ensureProjectSelected, hasActiveProject, handleStartNew],
|
||||
);
|
||||
|
||||
const handleClubPress = useCallback(() => {
|
||||
@@ -467,8 +456,8 @@ const Home = ({ navigation, route }) => {
|
||||
scrollEnabled={false}
|
||||
containerStyle={styles.page}
|
||||
contentContainerStyle={styles.pageContent}
|
||||
maxWidth={1600}
|
||||
width="100%"
|
||||
maxWidth={null}
|
||||
backgroundColor="#303438"
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
@@ -534,6 +523,8 @@ const Home = ({ navigation, route }) => {
|
||||
shareBtn
|
||||
title="Landing Page"
|
||||
backgroundImg={landingBackgroundImage}
|
||||
contentContainerStyle={styles.pageContent}
|
||||
backgroundColor="#303438"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -568,12 +559,9 @@ const styles = StyleSheet.create({
|
||||
page: {
|
||||
backgroundColor: "transparent",
|
||||
padding: 0,
|
||||
paddingLeft: isWeb ? 32 : 0,
|
||||
paddingRight: isWeb ? 32 : 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
width: "100%",
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
pageContent: {
|
||||
flexGrow: 1,
|
||||
@@ -584,7 +572,6 @@ const styles = StyleSheet.create({
|
||||
alignSelf: "stretch",
|
||||
alignItems: "stretch",
|
||||
justifyContent: "flex-start",
|
||||
position: "relative",
|
||||
},
|
||||
centerImage: {
|
||||
width: HOME_BACKGROUND_WIDTH + 120,
|
||||
@@ -625,6 +612,7 @@ const styles = StyleSheet.create({
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
marginBottom: 16,
|
||||
marginTop: 15,
|
||||
},
|
||||
cardsGrid: {
|
||||
width: "100%",
|
||||
@@ -632,6 +620,5 @@ const styles = StyleSheet.create({
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
rowGap: 20,
|
||||
columnGap: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { memo } from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
@@ -14,7 +14,6 @@ const StageCard = ({
|
||||
textAlign = "left",
|
||||
isLocked,
|
||||
onPress,
|
||||
lockSide = "right",
|
||||
}) => {
|
||||
const isImageOnLeft = imagePosition !== "right";
|
||||
const isTextRight = textAlign === "right";
|
||||
@@ -22,16 +21,41 @@ const StageCard = ({
|
||||
const imageBlock = (
|
||||
<View
|
||||
style={[
|
||||
styles.imageContainer,
|
||||
isImageOnLeft ? styles.imageLeft : styles.imageRight,
|
||||
{
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
zIndex: 10,
|
||||
},
|
||||
isImageOnLeft
|
||||
? {
|
||||
borderTopLeftRadius: 20,
|
||||
borderBottomLeftRadius: 20,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}
|
||||
: {
|
||||
borderTopRightRadius: 20,
|
||||
borderBottomRightRadius: 20,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ExpoImage
|
||||
source={image}
|
||||
contentFit="contain"
|
||||
style={[
|
||||
styles.image,
|
||||
isImageOnLeft ? styles.imageAlignRight : styles.imageAlignLeft,
|
||||
{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: 210,
|
||||
borderRadius: 0,
|
||||
},
|
||||
isImageOnLeft
|
||||
? { alignSelf: "flex-end" }
|
||||
: { alignSelf: "flex-start" },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
@@ -40,14 +64,26 @@ const StageCard = ({
|
||||
const header = (
|
||||
<View
|
||||
style={[
|
||||
styles.textHeader,
|
||||
isTextRight ? styles.textHeaderAlignEnd : styles.textHeaderAlignStart,
|
||||
{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
},
|
||||
isTextRight
|
||||
? { justifyContent: "flex-end" }
|
||||
: { justifyContent: "flex-start" },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.step,
|
||||
isTextRight ? styles.textRight : styles.textLeft,
|
||||
{
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
flexShrink: 1,
|
||||
},
|
||||
isTextRight ? { textAlign: "right" } : { textAlign: "left" },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@@ -61,16 +97,42 @@ const StageCard = ({
|
||||
tint="dark"
|
||||
intensity={30}
|
||||
style={[
|
||||
styles.textContainer,
|
||||
isImageOnLeft ? styles.textContainerRight : styles.textContainerLeft,
|
||||
isTextRight ? styles.alignEnd : styles.alignStart,
|
||||
{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
maxWidth: 380,
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 20,
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
},
|
||||
isImageOnLeft
|
||||
? {
|
||||
borderTopRightRadius: 20,
|
||||
borderBottomRightRadius: 20,
|
||||
}
|
||||
: {
|
||||
borderTopLeftRadius: 20,
|
||||
borderBottomLeftRadius: 20,
|
||||
},
|
||||
isTextRight ? { alignItems: "flex-end" } : { alignItems: "flex-start" },
|
||||
]}
|
||||
>
|
||||
{isLocked ? (
|
||||
<View
|
||||
style={[
|
||||
styles.lockBadge,
|
||||
isTextRight ? styles.lockBadgeLeft : styles.lockBadgeRight,
|
||||
{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.55)",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2,
|
||||
},
|
||||
isTextRight ? { left: 10 } : { right: 10 },
|
||||
]}
|
||||
>
|
||||
<FontAwesome name="lock" size={18} color={Palette.primary} />
|
||||
@@ -79,8 +141,13 @@ const StageCard = ({
|
||||
{header}
|
||||
<Text
|
||||
style={[
|
||||
styles.description,
|
||||
isTextRight ? styles.textRight : styles.textLeft,
|
||||
isTextRight ? { textAlign: "right" } : { textAlign: "left" },
|
||||
{
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
color: Palette.white,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
@@ -105,142 +172,21 @@ const StageCard = ({
|
||||
onPress={onPress}
|
||||
disabled={isLocked}
|
||||
style={({ pressed }) => [
|
||||
styles.card,
|
||||
pressed && !isLocked && styles.cardPressed,
|
||||
{
|
||||
width: "48%",
|
||||
},
|
||||
pressed && !isLocked && { opacity: 0.85 },
|
||||
]}
|
||||
>
|
||||
<View style={styles.inner}>{content}</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(StageCard);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: "48%",
|
||||
minWidth: 260,
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
borderRadius: 20,
|
||||
},
|
||||
cardPressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
inner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "stretch",
|
||||
},
|
||||
imageContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minHeight: 200,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
},
|
||||
imageLeft: {
|
||||
borderTopLeftRadius: 20,
|
||||
borderBottomLeftRadius: 20,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
imageRight: {
|
||||
borderTopRightRadius: 20,
|
||||
borderBottomRightRadius: 20,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
image: {
|
||||
width: "85%",
|
||||
height: "85%",
|
||||
minHeight: 200,
|
||||
borderRadius: 0,
|
||||
},
|
||||
imageAlignRight: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
imageAlignLeft: {
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
textContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 240,
|
||||
maxWidth: 320,
|
||||
minHeight: 120,
|
||||
maxHeight: 160,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 18,
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
},
|
||||
textContainerRight: {
|
||||
borderTopRightRadius: 18,
|
||||
borderBottomRightRadius: 18,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
marginLeft: -32,
|
||||
},
|
||||
textContainerLeft: {
|
||||
borderTopLeftRadius: 18,
|
||||
borderBottomLeftRadius: 18,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
marginRight: -32,
|
||||
},
|
||||
alignStart: {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
alignEnd: {
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
textHeader: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
},
|
||||
textHeaderAlignStart: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
textHeaderAlignEnd: {
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
step: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
flexShrink: 1,
|
||||
},
|
||||
description: {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
color: Palette.white,
|
||||
},
|
||||
textLeft: {
|
||||
textAlign: "left",
|
||||
},
|
||||
textRight: {
|
||||
textAlign: "right",
|
||||
},
|
||||
lockBadge: {
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.55)",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2,
|
||||
},
|
||||
lockBadgeLeft: {
|
||||
left: 10,
|
||||
},
|
||||
lockBadgeRight: {
|
||||
right: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -97,9 +97,9 @@ export default function AllMyList() {
|
||||
liked: {
|
||||
title: "Aucun playback liké pour le moment",
|
||||
description:
|
||||
"Ajoutez vos playbacks favoris à vos likes pour les retrouver instantanément.",
|
||||
"Ajoutez vos playback favoris à vos likes pour les retrouver instantanément.",
|
||||
cta: {
|
||||
label: "Voir les playbacks",
|
||||
label: "Voir les playback",
|
||||
action: () => navigate(Routes.Playbacks),
|
||||
},
|
||||
},
|
||||
@@ -110,7 +110,7 @@ export default function AllMyList() {
|
||||
}, [liked, scope]);
|
||||
|
||||
const loadingMessage = useMemo(() => {
|
||||
const target = scope === "playback" ? "playbacks" : "musiques";
|
||||
const target = scope === "playback" ? "playback" : "musiques";
|
||||
return liked
|
||||
? `Chargement des ${target} likés…`
|
||||
: `Chargement des ${target}…`;
|
||||
|
||||
@@ -37,10 +37,10 @@ const BackTracks = () => {
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
label="Mes Playbacks"
|
||||
label="Mes Playback"
|
||||
onPress={() =>
|
||||
navigate(Routes.AllMyList, {
|
||||
title: "Mes playbacks",
|
||||
title: "Mes playback",
|
||||
scope: "playback",
|
||||
liked: false,
|
||||
})
|
||||
|
||||
@@ -31,10 +31,10 @@ const LikedPlayback = () => {
|
||||
);
|
||||
return (
|
||||
<CardContainer
|
||||
label="Playbacks likés"
|
||||
label="Playback likés"
|
||||
onPress={() =>
|
||||
navigate(Routes.AllMyList, {
|
||||
title: "Playbacks likés",
|
||||
title: "Playback likés",
|
||||
scope: "playback",
|
||||
liked: true,
|
||||
})
|
||||
@@ -84,7 +84,7 @@ const LikedPlayback = () => {
|
||||
<GradientButton
|
||||
containerStyle={{ padding: 2 }}
|
||||
onPress={() => navigate(Routes.Playbacks)}
|
||||
title="Voir les playbacks"
|
||||
title="Voir les playback"
|
||||
></GradientButton>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -31,7 +31,7 @@ const ResearchHeader = ({
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{["Musiques", "Playbacks", "Profils"].map((item, index) => (
|
||||
{["Musiques", "Playback", "Profils"].map((item, index) => (
|
||||
<Pressable key={index} onPress={() => onPress(item)}>
|
||||
<BorderGradient
|
||||
gradientProps={{
|
||||
|
||||
@@ -133,7 +133,7 @@ const SearchResultsList = ({
|
||||
|
||||
const shouldShowPlaybacks =
|
||||
(!selected && (playbacks.length > 0 || playbacksLoading)) ||
|
||||
selected === "Playbacks";
|
||||
selected === "Playback";
|
||||
|
||||
const shouldShowUsers =
|
||||
(!selected && (users.length > 0 || usersLoading)) || selected === "Profils";
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useRoute } from "@react-navigation/native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import CreditAmount from "../components/CreditAmount";
|
||||
import Page from "../layouts/Page";
|
||||
import { background, subBadges } from "../assets";
|
||||
import { Palette, gutters } from "../styles";
|
||||
@@ -137,9 +138,16 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
) : null}
|
||||
</View>
|
||||
{coinsPerMonth !== null ? (
|
||||
<Text style={styles.coinsPerMonth}>
|
||||
{`+${coinsPerMonth} crédits / mois`}
|
||||
</Text>
|
||||
<View style={styles.coinsPerMonthRow}>
|
||||
<CreditAmount
|
||||
value={coinsPerMonth}
|
||||
showPlus
|
||||
textStyle={styles.coinsPerMonth}
|
||||
iconSize={16}
|
||||
accessibilityLabel={`+${coinsPerMonth} pièces par mois`}
|
||||
/>
|
||||
<Text style={styles.coinsPerMonthSuffix}>/ mois</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
@@ -782,11 +790,21 @@ const styles = StyleSheet.create({
|
||||
alignItems: "baseline",
|
||||
gap: 8,
|
||||
},
|
||||
coinsPerMonthRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
coinsPerMonth: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 13,
|
||||
color: Palette.primary,
|
||||
},
|
||||
coinsPerMonthSuffix: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 13,
|
||||
color: Palette.primary,
|
||||
},
|
||||
priceValue: {
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
fontSize: 22,
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Svg, { Circle } from "react-native-svg";
|
||||
import alert from "../../components/Alert";
|
||||
@@ -30,6 +30,11 @@ const log =
|
||||
? (...args) => console.log(LOG_PREFIX, ...args)
|
||||
: () => {};
|
||||
|
||||
const CAMERA_FACING_OPTIONS = [
|
||||
{ label: "Avant", value: "front" },
|
||||
{ label: "Arrière", value: "back" },
|
||||
];
|
||||
|
||||
const RecordPlayback = ({ route }) => {
|
||||
const { top, bottom } = useSafeAreaInsets();
|
||||
const { project } = route.params || {};
|
||||
@@ -63,6 +68,7 @@ const RecordPlayback = ({ route }) => {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [showProgress, setShowProgress] = useState(false);
|
||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
|
||||
const [cameraFacing, setCameraFacing] = useState("front");
|
||||
|
||||
// Musique
|
||||
const songUrl = project?.songUrl || null;
|
||||
@@ -612,7 +618,7 @@ const RecordPlayback = ({ route }) => {
|
||||
<CameraView
|
||||
ref={cameraRef}
|
||||
style={{ flex: 1 }}
|
||||
facing="front"
|
||||
facing={cameraFacing}
|
||||
mode="video"
|
||||
mute
|
||||
>
|
||||
@@ -626,6 +632,14 @@ const RecordPlayback = ({ route }) => {
|
||||
>
|
||||
<MusicLandHeader progress={9} onPressBack={goBack} />
|
||||
|
||||
<View style={{ marginTop: 12, alignItems: "flex-end" }}>
|
||||
<CameraFacingSelector
|
||||
value={cameraFacing}
|
||||
onChange={setCameraFacing}
|
||||
disabled={!permissionsGranted || isPreparing || isRecording}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1, marginTop: 11 }}>
|
||||
{/* Overlay de compte à rebours : on affiche 5→1 pour éviter l'effet visuel à 1 */}
|
||||
{isPreparing && countdown >= 1 && !showProgress && (
|
||||
@@ -836,3 +850,49 @@ const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const CameraFacingSelector = ({ value = "front", onChange, disabled = false }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
backgroundColor: "rgba(0,0,0,0.35)",
|
||||
borderRadius: 999,
|
||||
padding: 4,
|
||||
gap: 4,
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
}}
|
||||
pointerEvents={disabled ? "none" : "auto"}
|
||||
>
|
||||
{CAMERA_FACING_OPTIONS.map((option) => {
|
||||
const isActive = option.value === value;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => {
|
||||
if (typeof onChange === "function") onChange(option.value);
|
||||
}}
|
||||
disabled={isActive}
|
||||
style={{
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 999,
|
||||
backgroundColor: isActive ? Palette.white : "transparent",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isActive ? Palette.black : Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Image, Pressable, Text, View } from "react-native";
|
||||
import { background, icons } from "../../assets";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
@@ -32,14 +38,22 @@ const RecordedPlayback = ({ route }) => {
|
||||
const songUrl = project?.songUrl || null;
|
||||
console.log("video uri is : ", videoUri);
|
||||
// AUDIO PLAYER (expo-audio → seconds)
|
||||
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
|
||||
title: typeof project?.title === "string" ? project.title : "Sans titre",
|
||||
artist: typeof project?.userName === "string" ? project.userName : "MusicLand",
|
||||
artwork: project?.coverUrl || null,
|
||||
coverUrl: project?.coverUrl || null,
|
||||
metadata: { projectId: project?.id, screen: "RecordedPlayback" },
|
||||
});
|
||||
const audioPlayer = useSharedAudioPlayer(
|
||||
songUrl ? { uri: songUrl } : undefined,
|
||||
{
|
||||
id: project?.id
|
||||
? `recorded-${project.id}`
|
||||
: songUrl
|
||||
? `recorded-${songUrl}`
|
||||
: undefined,
|
||||
title: typeof project?.title === "string" ? project.title : "Sans titre",
|
||||
artist:
|
||||
typeof project?.userName === "string" ? project.userName : "MusicLand",
|
||||
artwork: project?.coverUrl || null,
|
||||
coverUrl: project?.coverUrl || null,
|
||||
metadata: { projectId: project?.id, screen: "RecordedPlayback" },
|
||||
},
|
||||
);
|
||||
|
||||
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
|
||||
const videoElRef = useRef(null);
|
||||
@@ -149,8 +163,11 @@ const RecordedPlayback = ({ route }) => {
|
||||
};
|
||||
|
||||
const wasPlayingRef = useRef(false);
|
||||
const onSeekStart = async () => {
|
||||
const isSeekingRef = useRef(false);
|
||||
const onSeekStart = useCallback(async () => {
|
||||
try {
|
||||
if (isSeekingRef.current) return;
|
||||
isSeekingRef.current = true;
|
||||
wasPlayingRef.current = !!audioPlayer?.playing;
|
||||
playbackEndedRef.current = false;
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.();
|
||||
@@ -158,80 +175,151 @@ const RecordedPlayback = ({ route }) => {
|
||||
videoElRef.current.pause();
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
const onSeekEnd = async () => {
|
||||
}, [audioPlayer]);
|
||||
const onSeekEnd = useCallback(async () => {
|
||||
try {
|
||||
if (!isSeekingRef.current) return;
|
||||
isSeekingRef.current = false;
|
||||
if (wasPlayingRef.current) {
|
||||
if (audioPlayer) await audioPlayer.play?.();
|
||||
if (videoElRef.current && videoUri)
|
||||
videoElRef.current.play().catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
}, [audioPlayer, videoUri]);
|
||||
|
||||
const handleTogglePlayback = useCallback(async () => {
|
||||
try {
|
||||
const duration = Number(progress?.durS || 0);
|
||||
const position = Number(progress?.posS || 0);
|
||||
const isAtEnd = duration > 0 && duration - position < 0.35;
|
||||
const isCurrentlyPlaying = !!audioPlayer?.playing;
|
||||
|
||||
if (isCurrentlyPlaying) {
|
||||
playbackEndedRef.current = false;
|
||||
await stopPlayback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAtEnd) {
|
||||
if (audioPlayer) await audioPlayer.seekTo?.(0);
|
||||
if (videoElRef.current && videoUri) {
|
||||
videoElRef.current.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
playbackEndedRef.current = false;
|
||||
if (songUrl && audioPlayer) {
|
||||
await audioPlayer.play?.();
|
||||
}
|
||||
if (videoElRef.current && videoUri) {
|
||||
videoElRef.current.muted = true;
|
||||
videoElRef.current.play().catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
}, [audioPlayer, songUrl, stopPlayback, progress, videoUri]);
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.playbackBG2} headerType="NONE">
|
||||
<MusicLandHeader progress={19} onPressBack={goBack} />
|
||||
<Page
|
||||
containerStyle={{
|
||||
alignItems: "none",
|
||||
}}
|
||||
backgroundImg={background.playbackBG2}
|
||||
headerType="NONE"
|
||||
>
|
||||
<MusicLandHeader
|
||||
progress={19}
|
||||
onPressBack={goBack}
|
||||
style={{
|
||||
marginBottom: 40,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, gap: 22, alignItems: "center" }}>
|
||||
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
|
||||
{videoUri ? (
|
||||
<View
|
||||
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
|
||||
{videoUri ? (
|
||||
<Pressable
|
||||
onPress={handleTogglePlayback}
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
aspectRatio: 9 / 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#00000066",
|
||||
overflow: "hidden",
|
||||
alignSelf: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
ref={videoElRef}
|
||||
src={videoUri}
|
||||
muted
|
||||
playsInline
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
aspectRatio: 9 / 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#00000066",
|
||||
overflow: "hidden",
|
||||
alignSelf: "center",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
>
|
||||
<video
|
||||
ref={videoElRef}
|
||||
src={videoUri}
|
||||
muted
|
||||
playsInline
|
||||
controls={false}
|
||||
/>
|
||||
{!progress.playing && (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
backgroundColor: "black",
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: [{ translateX: -36 }, { translateY: -36 }],
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "rgba(10, 5, 24, 0.75)",
|
||||
borderWidth: 1,
|
||||
borderColor: "#F94697",
|
||||
}}
|
||||
controls={false}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
// Placeholder quand pas de vidéo sur web
|
||||
<View
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
aspectRatio: 9 / 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#00000066",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: Palette.white, opacity: 0.8 }}>
|
||||
Aperçu vidéo non disponible sur le web
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
>
|
||||
<Image source={icons.play} style={{ width: 26, height: 26 }} />
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
) : (
|
||||
// Placeholder quand pas de vidéo sur web
|
||||
<View
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
aspectRatio: 9 / 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#00000066",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: Palette.white, opacity: 0.8 }}>
|
||||
Aperçu vidéo non disponible sur le web
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
alignSelf: "center",
|
||||
marginTop: 6,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View style={{ width: "100%" }}>
|
||||
<Slider
|
||||
value={fmtSeconds(progress.posS)} // mm:ss (seconds)
|
||||
maxValue={fmtSeconds(progress.durS)} // mm:ss (seconds)
|
||||
@@ -243,36 +331,36 @@ const RecordedPlayback = ({ route }) => {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
alignSelf: "center",
|
||||
marginTop: 4,
|
||||
gap: 12,
|
||||
<View
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||
maxWidth: "100%",
|
||||
alignSelf: "center",
|
||||
gap: 12,
|
||||
marginTop: 50,
|
||||
}}
|
||||
>
|
||||
<GradientButton
|
||||
title="Je valide"
|
||||
onPress={() => {
|
||||
navigate(Routes.DownloadSongs, {
|
||||
action: "playback",
|
||||
uri: videoUri || null, // peut être null sur web
|
||||
project,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GradientButton
|
||||
title="Je valide"
|
||||
onPress={() => {
|
||||
navigate(Routes.DownloadSongs, {
|
||||
action: "playback",
|
||||
uri: videoUri || null, // peut être null sur web
|
||||
project,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Recommencer"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await stopPlayback();
|
||||
} catch {}
|
||||
navigate(Routes.RecordPlayback, { project });
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Recommencer"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await stopPlayback();
|
||||
} catch {}
|
||||
navigate(Routes.RecordPlayback, { project });
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import React, {
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
@@ -85,7 +86,7 @@ const Playbacks = () => {
|
||||
loadMore?.();
|
||||
}
|
||||
},
|
||||
[playbacks?.length, loadMore]
|
||||
[playbacks?.length, loadMore],
|
||||
);
|
||||
|
||||
const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => {
|
||||
@@ -193,7 +194,7 @@ const Playbacks = () => {
|
||||
handleActiveIndexChange(idx);
|
||||
} catch (_e) {}
|
||||
},
|
||||
[windowHeight, handleActiveIndexChange]
|
||||
[windowHeight, handleActiveIndexChange],
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
@@ -204,7 +205,7 @@ const Playbacks = () => {
|
||||
handleActiveIndexChange(idx);
|
||||
} catch (_e) {}
|
||||
},
|
||||
[windowHeight, handleActiveIndexChange]
|
||||
[windowHeight, handleActiveIndexChange],
|
||||
);
|
||||
|
||||
// programmatic jump to focus item
|
||||
@@ -228,7 +229,7 @@ const Playbacks = () => {
|
||||
offset: windowHeight * index,
|
||||
index,
|
||||
}),
|
||||
[windowHeight]
|
||||
[windowHeight],
|
||||
);
|
||||
|
||||
const total = playbacks.length;
|
||||
@@ -272,6 +273,47 @@ const Playbacks = () => {
|
||||
return `${activeIndex + 1}/${total}`;
|
||||
}, [total, activeIndex, hasMore, loading]);
|
||||
|
||||
const scrollToPlayback = useCallback(
|
||||
(direction) => {
|
||||
if (!direction || !listRef.current) return;
|
||||
const totalItems = playbacks.length;
|
||||
if (totalItems <= 0) return;
|
||||
const targetIndex = Math.max(
|
||||
0,
|
||||
Math.min(activeIndex + direction, totalItems - 1),
|
||||
);
|
||||
if (targetIndex === activeIndex) {
|
||||
if (direction > 0 && hasMore) {
|
||||
loadMore?.();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const targetOffset = targetIndex * windowHeight;
|
||||
try {
|
||||
listRef.current.scrollToOffset({
|
||||
offset: targetOffset,
|
||||
animated: true,
|
||||
});
|
||||
} catch (_e) {
|
||||
try {
|
||||
listRef.current.scrollToIndex({
|
||||
index: targetIndex,
|
||||
animated: true,
|
||||
});
|
||||
} catch (__e) {}
|
||||
}
|
||||
handleActiveIndexChange(targetIndex);
|
||||
},
|
||||
[
|
||||
activeIndex,
|
||||
handleActiveIndexChange,
|
||||
hasMore,
|
||||
loadMore,
|
||||
playbacks.length,
|
||||
windowHeight,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
{activeVideoUrl ? (
|
||||
@@ -324,16 +366,23 @@ const Playbacks = () => {
|
||||
scrollEventThrottle={16}
|
||||
/>
|
||||
{showIndicators && (
|
||||
<View style={styles.indicatorContainer} pointerEvents="none">
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
resizeMode="contain"
|
||||
style={[
|
||||
styles.indicatorArrow,
|
||||
styles.indicatorArrowUp,
|
||||
!canScrollUp && styles.indicatorArrowDisabled,
|
||||
]}
|
||||
/>
|
||||
<View style={styles.indicatorContainer} pointerEvents="box-none">
|
||||
<Pressable
|
||||
onPress={() => scrollToPlayback(-1)}
|
||||
hitSlop={12}
|
||||
disabled={!canScrollUp}
|
||||
style={styles.indicatorArrowTouch}
|
||||
>
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
resizeMode="contain"
|
||||
style={[
|
||||
styles.indicatorArrow,
|
||||
styles.indicatorArrowUp,
|
||||
!canScrollUp && styles.indicatorArrowDisabled,
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
<View style={styles.indicatorDotsWrapper}>
|
||||
{indicatorItems.map((item, index) => (
|
||||
<View
|
||||
@@ -357,14 +406,21 @@ const Playbacks = () => {
|
||||
{/* {indicatorLabel ? (
|
||||
<Text style={styles.indicatorLabel}>{indicatorLabel}</Text>
|
||||
) : null} */}
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
resizeMode="contain"
|
||||
style={[
|
||||
styles.indicatorArrow,
|
||||
!canScrollDown && styles.indicatorArrowDisabled,
|
||||
]}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={() => scrollToPlayback(1)}
|
||||
hitSlop={12}
|
||||
disabled={!canScrollDown}
|
||||
style={styles.indicatorArrowTouch}
|
||||
>
|
||||
<Image
|
||||
source={icons.chevronDown}
|
||||
resizeMode="contain"
|
||||
style={[
|
||||
styles.indicatorArrow,
|
||||
!canScrollDown && styles.indicatorArrowDisabled,
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -377,11 +433,8 @@ export default Playbacks;
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
// overscrollBehavior: "none",
|
||||
// position: "relative",
|
||||
},
|
||||
backgroundVideoWrapper: {
|
||||
// ...StyleSheet.absoluteFillObject,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
@@ -437,6 +490,10 @@ const styles = StyleSheet.create({
|
||||
tintColor: Palette.white,
|
||||
opacity: 0.8,
|
||||
},
|
||||
indicatorArrowTouch: {
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
indicatorArrowUp: {
|
||||
transform: [{ rotate: "180deg" }],
|
||||
},
|
||||
|
||||
@@ -26,14 +26,14 @@ const DownloadPrices = () => {
|
||||
price: "6,99€",
|
||||
},
|
||||
{
|
||||
title: `3 ${action === "playback" ? "Playbacks" : "chansons"}`,
|
||||
title: `3 ${action === "playback" ? "Playback" : "chansons"}`,
|
||||
description: `Ce ${
|
||||
action === "playback" ? "Playback" : "morceau"
|
||||
} et les 2 prochains que tu crées`,
|
||||
price: "12,99€",
|
||||
},
|
||||
{
|
||||
title: `5 ${action === "playback" ? "Playbacks" : "chansons"}`,
|
||||
title: `5 ${action === "playback" ? "Playback" : "chansons"}`,
|
||||
description: `Ce ${
|
||||
action === "playback" ? "Playback" : "morceau"
|
||||
} et les 4 prochains que tu crées`,
|
||||
|
||||
@@ -1,59 +1,101 @@
|
||||
import { View, Text, Image } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { background, img } from "../../assets";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import Style, { size } from "../../styles/Style";
|
||||
import { size } from "../../styles/Style";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { Routes } from "../../navigation";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
|
||||
const STREAM_PRICES = [
|
||||
{
|
||||
title: "Je diffuse ma chanson",
|
||||
description: "Ce morceau uniquement",
|
||||
price: "1,99€ / mois",
|
||||
},
|
||||
{
|
||||
title: "Je diffuse 3 chansons",
|
||||
description: "Ce morceau et les 2 prochains que tu crées",
|
||||
price: "3,99€ / mois",
|
||||
},
|
||||
{
|
||||
title: "Illimité",
|
||||
price: "14,99€ / mois",
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
|
||||
"trialing",
|
||||
"active",
|
||||
"past_due",
|
||||
"unpaid",
|
||||
]);
|
||||
|
||||
const PLAYBACK_STREAM_PRICES = [
|
||||
{
|
||||
title: "Je créer et diffuse mon Playback",
|
||||
description: "Ce morceau uniquement",
|
||||
price: "6,99€ / mois",
|
||||
},
|
||||
{
|
||||
title: "3 Playbacks",
|
||||
description: "Ce morceau et les 2 prochains que tu crées",
|
||||
price: "12,99€ / mois",
|
||||
},
|
||||
{
|
||||
title: "Illimité",
|
||||
price: "20€ / mois",
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
const pickStatus = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed.toLowerCase() : null;
|
||||
};
|
||||
|
||||
const hasActiveSubscription = (userData) => {
|
||||
if (!userData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const statusCandidates = [
|
||||
pickStatus(userData?.stripeSubscriptionStatus),
|
||||
pickStatus(userData?.stripeSubscription?.status),
|
||||
pickStatus(userData?.stripeSubscription?.stripeSubscriptionStatus),
|
||||
pickStatus(userData?.stripeSubscription?.metadata?.status),
|
||||
].filter(Boolean);
|
||||
|
||||
if (
|
||||
statusCandidates.some((status) =>
|
||||
ACTIVE_SUBSCRIPTION_STATUSES.has(status),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const premiumUntil = userData?.premiumUntil;
|
||||
if (premiumUntil) {
|
||||
const asDate =
|
||||
premiumUntil?.toDate?.() ||
|
||||
(premiumUntil?.seconds ? new Date(premiumUntil.seconds * 1000) : null);
|
||||
|
||||
if (asDate && asDate > new Date()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (userData?.premium?.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const StreamSong = () => {
|
||||
const params = useRoute().params;
|
||||
const action = params?.action;
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
|
||||
const DATA = action === "playback" ? PLAYBACK_STREAM_PRICES : STREAM_PRICES;
|
||||
const { currentUserData = null } = useUser() || {};
|
||||
const isPlaybackFlow = action === "playback";
|
||||
const userHasActiveSubscription = useMemo(
|
||||
() => hasActiveSubscription(currentUserData),
|
||||
[currentUserData],
|
||||
);
|
||||
const headingText = isPlaybackFlow
|
||||
? "Publier ton playback"
|
||||
: "Publier ta chanson";
|
||||
const descriptionText = userHasActiveSubscription
|
||||
? `Ton abonnement est actif, tu peux publier ${
|
||||
isPlaybackFlow ? "ce playback" : "cette chanson"
|
||||
} sur MusicLand et participer au concours.`
|
||||
: `Pour publier ${isPlaybackFlow ? "ton playback" : "ta chanson"} sur la plateforme, tu dois rejoindre le Club MusicLand.`;
|
||||
const ctaLabel = userHasActiveSubscription
|
||||
? isPlaybackFlow
|
||||
? "Publier mon playback"
|
||||
: "Publier ma chanson"
|
||||
: "Rejoindre le Club MusicLand";
|
||||
const handlePrimaryAction = useCallback(() => {
|
||||
if (userHasActiveSubscription) {
|
||||
navigate(Routes.SongRelease, { action });
|
||||
return;
|
||||
}
|
||||
navigate(Routes.Payments);
|
||||
}, [action, userHasActiveSubscription]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -90,8 +132,7 @@ const StreamSong = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Diffuser ta chanson{"\n"}
|
||||
Tarifs
|
||||
{headingText}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -101,91 +142,36 @@ const StreamSong = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Si tu désires diffuser ta chanson ou tes chansons sur la
|
||||
plateforme de MusicLand et participer au concours et être dans
|
||||
le top 3 voici le tarif.
|
||||
{descriptionText}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ gap: 10 }}>
|
||||
{DATA.map((item, index) => (
|
||||
<CreateLyricsHeader
|
||||
key={index}
|
||||
colors={
|
||||
selectedIndex === index
|
||||
? ["#F94697", "#7023F7"]
|
||||
: [Palette.tran, Palette.tran]
|
||||
}
|
||||
tint={!item.recommended ? "dark" : "light"}
|
||||
onPress={() => {
|
||||
setSelectedIndex(index);
|
||||
navigate(Routes.SongRelease, {
|
||||
action,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingTop: 5 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.description && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={{ alignSelf: "flex-end", marginTop: 5 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{item.price}
|
||||
</Text>
|
||||
</View>
|
||||
{item.recommended && (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 10,
|
||||
height: 26,
|
||||
...Style.containerCenter,
|
||||
borderRadius: 100,
|
||||
position: "absolute",
|
||||
backgroundColor: "#FFFFFF33",
|
||||
bottom: 8,
|
||||
left: 10,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||
bottom: -1,
|
||||
}}
|
||||
>
|
||||
Recommandé
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</CreateLyricsHeader>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</CreateLyricsHeader>
|
||||
<View style={{ marginTop: 24, gap: 12 }}>
|
||||
<GradientButton
|
||||
title={ctaLabel}
|
||||
onPress={handlePrimaryAction}
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
maxWidth: 320,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
{!userHasActiveSubscription && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
Rejoins le club pour débloquer la publication de tes créations et
|
||||
tenter de grimper dans le classement.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Alert, Platform, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import CreditAmount from "../../components/CreditAmount";
|
||||
import { background } from "../../assets";
|
||||
import { getFunctionsClient } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
@@ -44,19 +45,6 @@ const PERIOD_LABELS = {
|
||||
|
||||
const PAGE_BACKGROUND_COLOR = "#303438";
|
||||
|
||||
const formatCoinsAmount = (value) => {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
} catch (_error) {
|
||||
return `${Math.round(value)}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColors = (status) => {
|
||||
switch (status) {
|
||||
case "trialing":
|
||||
@@ -384,11 +372,6 @@ const ManageSubscription = ({ navigation }) => {
|
||||
typeof coinsPerMonth === "number" && Number.isFinite(coinsPerMonth)
|
||||
? Math.round(coinsPerMonth)
|
||||
: null;
|
||||
const coinsPerMonthLabel =
|
||||
normalizedCoins !== null
|
||||
? `${formatCoinsAmount(normalizedCoins)} coins`
|
||||
: null;
|
||||
|
||||
const nextGrantTimestamp =
|
||||
currentUserData?.subscriptionNextGrantAt ||
|
||||
currentUserData?.subscriptionGrantNextAt ||
|
||||
@@ -434,11 +417,6 @@ const ManageSubscription = ({ navigation }) => {
|
||||
"Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment.",
|
||||
);
|
||||
}
|
||||
if (isAnnual && coinsPerMonthLabel) {
|
||||
helperMessages.push(
|
||||
`Tes crédits sont versés chaque mois (${coinsPerMonthLabel}).`,
|
||||
);
|
||||
}
|
||||
const helperMessage = helperMessages.join("\n");
|
||||
|
||||
return {
|
||||
@@ -462,7 +440,6 @@ const ManageSubscription = ({ navigation }) => {
|
||||
currentUserData?.stripeSubscription?.subscriptionId ||
|
||||
null,
|
||||
coinsPerMonth: normalizedCoins,
|
||||
coinsPerMonthLabel,
|
||||
isAnnual,
|
||||
nextGrantDate: resolvedNextGrantDate,
|
||||
nextGrantLabel,
|
||||
@@ -614,9 +591,15 @@ const ManageSubscription = ({ navigation }) => {
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Crédits mensuels</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{subscriptionInfo.coinsPerMonthLabel || "—"}
|
||||
</Text>
|
||||
{typeof subscriptionInfo.coinsPerMonth === "number" ? (
|
||||
<CreditAmount
|
||||
value={subscriptionInfo.coinsPerMonth}
|
||||
textStyle={styles.detailValue}
|
||||
iconSize={18}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.detailValue}>—</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{subscriptionInfo.isAnnual && subscriptionInfo.nextGrantLabel ? (
|
||||
@@ -659,6 +642,20 @@ const ManageSubscription = ({ navigation }) => {
|
||||
{subscriptionInfo.helperMessage}
|
||||
</Text>
|
||||
) : null}
|
||||
{subscriptionInfo.isAnnual &&
|
||||
typeof subscriptionInfo.coinsPerMonth === "number" ? (
|
||||
<View style={styles.helperInlineRow}>
|
||||
<Text style={styles.helperText}>
|
||||
Tes pièces sont versées chaque mois (
|
||||
</Text>
|
||||
<CreditAmount
|
||||
value={subscriptionInfo.coinsPerMonth}
|
||||
textStyle={styles.helperText}
|
||||
iconSize={14}
|
||||
/>
|
||||
<Text style={styles.helperText}>)</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{successMessage ? (
|
||||
@@ -767,6 +764,12 @@ const styles = StyleSheet.create({
|
||||
lineHeight: 18,
|
||||
color: Palette.grayMid,
|
||||
},
|
||||
helperInlineRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 4,
|
||||
},
|
||||
successMessage: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 14,
|
||||
|
||||
@@ -13,6 +13,7 @@ import Page from "../../layouts/Page";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import CreditAmount from "../../components/CreditAmount";
|
||||
|
||||
const ORDER_TYPE_LABELS = {
|
||||
GIFT: "Crédit offert",
|
||||
@@ -21,19 +22,6 @@ const ORDER_TYPE_LABELS = {
|
||||
SUBSCRIPTION: "Abonnement",
|
||||
};
|
||||
|
||||
const formatCoins = (amount) => {
|
||||
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
||||
return "0";
|
||||
}
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
} catch (_error) {
|
||||
return `${amount}`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateParts = (date) => {
|
||||
if (!date) {
|
||||
return {
|
||||
@@ -133,9 +121,10 @@ const OrderHistory = () => {
|
||||
? item.amount
|
||||
: 0;
|
||||
const isPositive = amountValue > 0;
|
||||
const amountLabel = `${isPositive ? "+" : ""}${formatCoins(amountValue)} ${
|
||||
Math.abs(amountValue) === 1 ? "coin" : "coins"
|
||||
}`;
|
||||
const amountTextStyle = [
|
||||
styles.orderAmount,
|
||||
isPositive ? styles.amountPositive : styles.amountNegative,
|
||||
];
|
||||
|
||||
const { dateLabel } = formatDateParts(item.createdAt);
|
||||
|
||||
@@ -182,14 +171,12 @@ const OrderHistory = () => {
|
||||
|
||||
return (
|
||||
<View style={styles.orderCard}>
|
||||
<Text
|
||||
style={[
|
||||
styles.orderAmount,
|
||||
isPositive ? styles.amountPositive : styles.amountNegative,
|
||||
]}
|
||||
>
|
||||
{amountLabel}
|
||||
</Text>
|
||||
<CreditAmount
|
||||
value={amountValue}
|
||||
showPlus
|
||||
textStyle={amountTextStyle}
|
||||
iconSize={18}
|
||||
/>
|
||||
<Text style={styles.orderReason}>{reasonLabel}</Text>
|
||||
<Text style={styles.orderTimestamp}>{dateLabel}</Text>
|
||||
</View>
|
||||
|
||||
@@ -508,7 +508,7 @@ const Profile = () => {
|
||||
}}
|
||||
>
|
||||
{/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
|
||||
{["Chansons", "Playbacks"].map((item, index) => (
|
||||
{["Chansons", "Playback"].map((item, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => onPressMenu(item)}
|
||||
@@ -540,7 +540,7 @@ const Profile = () => {
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
{selected === "Playbacks" && (
|
||||
{selected === "Playback" && (
|
||||
<MusicListSection
|
||||
data={displayedPlaybacks}
|
||||
emptyText="Aucun playback pour le moment"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Image, StyleSheet, View } from "react-native";
|
||||
import { ai, background } from "../../assets";
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||
@@ -8,9 +8,17 @@ import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { gutters } from "../../styles";
|
||||
import { useUserData } from "../../providers/UserDataProvider";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
|
||||
const Compose = () => {
|
||||
const [showIntro, setShowIntro] = useState(true);
|
||||
const { videos } = useUserData();
|
||||
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setVideoUrl(isWeb ? videos.malikWeb : videos?.malik);
|
||||
}, []);
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<Image source={ai.malik} style={styles.img} resizeMode="contain" />
|
||||
@@ -36,8 +44,9 @@ const Compose = () => {
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
visible={showIntro}
|
||||
onClose={() => setShowIntro(false)}
|
||||
visible={!!videoUrl}
|
||||
url={videoUrl}
|
||||
onClose={() => setVideoUrl(null)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import AppAlert from "../../components/Alert";
|
||||
import CoinIcon from "../../components/CoinIcon";
|
||||
import CreditAmount from "../../components/CreditAmount";
|
||||
import firebase, { getFunctionsClient } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
@@ -65,16 +65,6 @@ const ComposeSong = () => {
|
||||
return 0;
|
||||
}, [currentUserData?.coins]);
|
||||
|
||||
const formattedCoinBalance = useMemo(() => {
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(coinBalance);
|
||||
} catch (_error) {
|
||||
return `${coinBalance}`;
|
||||
}
|
||||
}, [coinBalance]);
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
@@ -357,19 +347,18 @@ const ComposeSong = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte{" "}
|
||||
Cette action coûte
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
<CreditAmount
|
||||
value={MUSIC_GENERATION_COIN_COST}
|
||||
textStyle={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{MUSIC_GENERATION_COIN_COST}
|
||||
</Text>
|
||||
<CoinIcon size={18} />
|
||||
iconSize={18}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -398,9 +387,18 @@ const ComposeSong = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance}
|
||||
Solde disponible :
|
||||
</Text>
|
||||
<CoinIcon size={16} />
|
||||
<CreditAmount
|
||||
value={coinBalance}
|
||||
textStyle={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
iconSize={16}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import AppAlert from "../../components/Alert";
|
||||
import CoinIcon from "../../components/CoinIcon";
|
||||
import CreditAmount from "../../components/CreditAmount";
|
||||
import firebase, { getFunctionsClient } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
@@ -68,16 +68,6 @@ const ComposeSong = () => {
|
||||
return 0;
|
||||
}, [currentUserData?.coins]);
|
||||
|
||||
const formattedCoinBalance = useMemo(() => {
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(coinBalance);
|
||||
} catch (_error) {
|
||||
return `${coinBalance}`;
|
||||
}
|
||||
}, [coinBalance]);
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
@@ -386,19 +376,18 @@ const ComposeSong = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte{" "}
|
||||
Cette action coûte
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
<CreditAmount
|
||||
value={MUSIC_GENERATION_COIN_COST}
|
||||
textStyle={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{MUSIC_GENERATION_COIN_COST}
|
||||
</Text>
|
||||
<CoinIcon size={18} />
|
||||
iconSize={18}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -427,9 +416,18 @@ const ComposeSong = () => {
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance}
|
||||
Solde disponible :
|
||||
</Text>
|
||||
<CoinIcon size={16} />
|
||||
<CreditAmount
|
||||
value={coinBalance}
|
||||
textStyle={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
iconSize={16}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Image, Platform, Text, View } from "react-native";
|
||||
import { Platform, Text, View } from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { ai } from "../../assets";
|
||||
import alert from "../../components/Alert";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import ProgressBar from "../../components/ProgressBar";
|
||||
@@ -275,7 +274,6 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 20,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#0F0C1933",
|
||||
@@ -284,12 +282,9 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
<BlurView
|
||||
intensity={Platform.OS !== "ios" ? 10 : 40}
|
||||
tint="dark"
|
||||
style={{ flex: 1, padding: 10, paddingBottom: 20 }}
|
||||
// experimentalBlurMethod={
|
||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
// }
|
||||
style={{ padding: 10, paddingBottom: 20 }}
|
||||
>
|
||||
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
|
||||
<View style={{ justifyContent: "flex-end", gap: 20 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { background } from "../../assets";
|
||||
@@ -12,18 +12,22 @@ import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUserData } from "../../providers/UserDataProvider";
|
||||
import { Palette, gutters } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo.web";
|
||||
|
||||
const CTA_BUTTON_HEIGHT = 58;
|
||||
const CTA_BUTTON_MAX_WIDTH = 520;
|
||||
|
||||
const WritingLyrics = () => {
|
||||
const {
|
||||
createNewProject,
|
||||
selectedProjectId,
|
||||
updateProjectData,
|
||||
} = useUserData();
|
||||
const { createNewProject, selectedProjectId, updateProjectData, videos } =
|
||||
useUserData();
|
||||
const { setIsLoading } = useMinuit();
|
||||
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setVideoUrl(isWeb ? videos.celineWeb : videos?.celine);
|
||||
}, []);
|
||||
|
||||
const startWriting = React.useCallback(async () => {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
@@ -55,17 +59,9 @@ const WritingLyrics = () => {
|
||||
backgroundImg={isWeb ? background.libraryBgWeb : background.writingBG}
|
||||
headerType="NONE"
|
||||
>
|
||||
{/* <Image source={ai.nathalie} style={styles.img} resizeMode="contain" /> */}
|
||||
<MusicLandHeader onPressBack={goBack} />
|
||||
<View style={styles.contentWrapper}>
|
||||
<View style={styles.heroContainer}>
|
||||
{/* <Text style={styles.heroTitle}>
|
||||
{strings.writing.onboarding.heroTitle}
|
||||
</Text>
|
||||
<Text style={styles.heroSubtitle}>
|
||||
{strings.writing.onboarding.heroSubtitle}
|
||||
</Text> */}
|
||||
</View>
|
||||
<View style={styles.heroContainer}></View>
|
||||
<View style={styles.ctaGroup}>
|
||||
<GradientButton
|
||||
size="large"
|
||||
@@ -81,6 +77,11 @@ const WritingLyrics = () => {
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
url={videoUrl}
|
||||
visible={!!videoUrl}
|
||||
onClose={() => setVideoUrl(null)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||