last fixes, home and videos

This commit is contained in:
Thomas Demirdjian
2025-11-10 14:24:16 +01:00
parent d7d9211fbe
commit dd4cf9b4d4
40 changed files with 984 additions and 647 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ exports.GEMINI_API_KEY = "AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y";
exports.SUNO_API_KEY = "c1636e04f606811511e19ec6e1545aa6"; // api key exports.SUNO_API_KEY = "c1636e04f606811511e19ec6e1545aa6"; // api key
exports.RESEND_API_KEY = "re_"; exports.RESEND_API_KEY = "re_NLcHmYiz_4LoFrzvPBbShNQBgNGTQmBbu";
exports.STRIPE_SECRET_KEY = exports.STRIPE_SECRET_KEY =
"sk_test_51SPfcjCzf2o5bDRdUFGNrQYIE271EDfS2Ucn31f98Ublttcl1EBNRoOoJX1RfXXzHp7mKRGrIlCG24biiqUZ2YMh00s9WODluu"; "sk_test_51SPfcjCzf2o5bDRdUFGNrQYIE271EDfS2Ucn31f98Ublttcl1EBNRoOoJX1RfXXzHp7mKRGrIlCG24biiqUZ2YMh00s9WODluu";
+81 -20
View File
@@ -7,7 +7,6 @@ const {
} = require("../config/suno"); } = require("../config/suno");
const { SUNO_API_KEY } = require("../config/keys"); const { SUNO_API_KEY } = require("../config/keys");
const axios = require("axios"); const axios = require("axios");
const admin = require("firebase-admin");
const { FieldValue } = require("firebase-admin/firestore"); const { FieldValue } = require("firebase-admin/firestore");
const { refList } = require("../index"); const { refList } = require("../index");
@@ -41,7 +40,7 @@ const STRUCTURE_ALIASES = {
"intro instrumentale longue": "long_intro", "intro instrumentale longue": "long_intro",
"pré-refrain": "pre_refrain_instrumental", "pré-refrain": "pre_refrain_instrumental",
"pre-refrain": "pre_refrain_instrumental", "pre-refrain": "pre_refrain_instrumental",
"pre_refrain": "pre_refrain_instrumental", pre_refrain: "pre_refrain_instrumental",
"pre chorus": "pre_refrain_instrumental", "pre chorus": "pre_refrain_instrumental",
"pre-chorus": "pre_refrain_instrumental", "pre-chorus": "pre_refrain_instrumental",
prechorus: "pre_refrain_instrumental", prechorus: "pre_refrain_instrumental",
@@ -62,7 +61,9 @@ const STRUCTURE_ALIASES = {
}; };
const normalizeStructureValue = (value) => { const normalizeStructureValue = (value) => {
const raw = String(value || "").trim().toLowerCase(); const raw = String(value || "")
.trim()
.toLowerCase();
if (!raw) return ""; if (!raw) return "";
if (STRUCTURE_PROMPT_LABELS[raw]) return raw; if (STRUCTURE_PROMPT_LABELS[raw]) return raw;
if (STRUCTURE_ALIASES[raw]) return STRUCTURE_ALIASES[raw]; if (STRUCTURE_ALIASES[raw]) return STRUCTURE_ALIASES[raw];
@@ -160,9 +161,7 @@ const moderationIndicatesBlock = (moderation = {}, fallbackText = "") => {
const excerpts = Array.isArray(moderation.excerpts) const excerpts = Array.isArray(moderation.excerpts)
? moderation.excerpts ? moderation.excerpts
: []; : [];
const reasons = Array.isArray(moderation.reasons) const reasons = Array.isArray(moderation.reasons) ? moderation.reasons : [];
? moderation.reasons
: [];
const keywordDetected = (value) => const keywordDetected = (value) =>
typeof value === "string" && TOXIC_KEYWORDS.some((rx) => rx.test(value)); typeof value === "string" && TOXIC_KEYWORDS.some((rx) => rx.test(value));
@@ -182,12 +181,7 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
style = "", style = "",
audience = "", audience = "",
emotion = "", emotion = "",
structure: rawStructure = [ structure: rawStructure = ["couplet", "refrain", "couplet", "refrain"],
"couplet",
"refrain",
"couplet",
"refrain",
],
rhymes = "", rhymes = "",
} = data; } = data;
@@ -248,14 +242,14 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
const system = ` const system = `
Tu es un parolier expert en chanson française moderne et populaire. Tu es un parolier expert en chanson française moderne et populaire.
Tu écris des chansons marquantes, accessibles, sur des thématiques d'innovation, d'esprit d'équipe et de créativité digitale.
Ton écriture doit toucher à la fois l'humain, le collectif et la passion d'entreprendre.
Analyse attentivement les informations placées entre balises XML-like. Analyse attentivement les informations placées entre balises XML-like.
Génère un titre accrocheur et mémorable en plus des paroles, tout en respectant strictement la structure demandée. Génère un titre accrocheur et mémorable en plus des paroles, tout en respectant strictement la structure demandée.
Retourne uniquement un JSON conforme au schéma fourni, sans ajouter d'autres textes. Retourne uniquement un JSON conforme au schéma fourni, sans ajouter d'autres textes.
`.trim(); `.trim();
const structureTags = (Array.isArray(promptStructure) ? promptStructure : []) const structureTags = (
Array.isArray(promptStructure) ? promptStructure : []
)
.map((part, index) => { .map((part, index) => {
const content = part || ""; const content = part || "";
return ` <SECTION ordre="${index + 1}">${content}</SECTION>`; return ` <SECTION ordre="${index + 1}">${content}</SECTION>`;
@@ -314,14 +308,12 @@ ${structureTags || fallbackStructureTags}
lyrics: z lyrics: z
.string() .string()
.describe( .describe(
"Paroles de la section, chaque ligne séparée " + "Paroles de la section, chaque ligne séparée par un retour à la ligne",
"par un retour à la ligne",
), ),
}), }),
) )
.describe( .describe(
"Paroles de la chanson sous forme de tableau de sections " + "Paroles de la chanson sous forme de tableau de sections structurées.",
"structurées.",
), ),
lyricsDescription: z lyricsDescription: z
.string() .string()
@@ -339,6 +331,74 @@ ${structureTags || fallbackStructureTags}
} }
}); });
const severityScore = (severity = "") => {
const normalized = String(severity || "").toLowerCase();
switch (normalized) {
case "critical":
return 4;
case "high":
return 3;
case "medium":
return 2;
case "low":
return 1;
default:
return 0;
}
};
const softenModerationDecision = (rawResult = {}) => {
const result = { ...rawResult };
const excerpts = Array.isArray(result.excerpts) ? result.excerpts : [];
const reasons = Array.isArray(result.reasons) ? result.reasons : [];
const highestSeverity = excerpts.reduce(
(max, excerpt) => Math.max(max, severityScore(excerpt?.severity)),
0,
);
const score =
typeof result.score === "number" && !Number.isNaN(result.score)
? Math.min(Math.max(result.score, 0), 1)
: 0;
const narrativeHint = reasons.some((reason) =>
/narrati|story|persona|fiction|metaphor|metaphore|récit|roleplay|contexte/i.test(
reason || "",
),
);
const adjustments = [];
if (result.blocked) {
const shouldUnblockLowSeverity = highestSeverity <= 1 && score < 0.65;
if (shouldUnblockLowSeverity) {
result.blocked = false;
adjustments.push("auto-unblock-low-severity");
}
}
if (!result.blocked && result.flagged) {
const lowSignal = highestSeverity <= 1 && score < 0.4;
const contextual = narrativeHint && highestSeverity <= 2 && score < 0.55;
if (lowSignal) {
result.flagged = false;
adjustments.push("drop-flag-low-signal");
} else if (contextual) {
result.flagged = false;
adjustments.push("drop-flag-contextual");
}
}
return {
...result,
moderationAdjustments: adjustments,
moderationCalibration: {
highestSeverity,
score,
},
};
};
// Vérifie la toxicité des paroles via Gemini et retourne un résultat structuré // Vérifie la toxicité des paroles via Gemini et retourne un résultat structuré
// Nom Cloud Function: lyrics-analyseLyricsToxicity // Nom Cloud Function: lyrics-analyseLyricsToxicity
exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => { exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
@@ -364,10 +424,11 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
}); });
const parsed = requestSchema.parse(data || {}); const parsed = requestSchema.parse(data || {});
const result = await analyseLyrics({ const aiResult = await analyseLyrics({
title: parsed.title || "", title: parsed.title || "",
lyrics: parsed.lyrics, lyrics: parsed.lyrics,
}); });
const result = softenModerationDecision(aiResult);
// Prépare un message lisible pour le front // Prépare un message lisible pour le front
const highCats = Object.entries(result.categories || {}) const highCats = Object.entries(result.categories || {})
Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

After

Width:  |  Height:  |  Size: 703 KiB

-2
View File
@@ -219,8 +219,6 @@ export const ai = {
malik, malik,
bena, bena,
john, john,
leftIcon: require("./UI/leftIcon.png"),
rightIcon: require("./UI/rightIcon.png"),
}; };
export const videos = { export const videos = {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

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