last fixes, home and videos
@@ -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";
|
||||||
|
|||||||
@@ -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 || {})
|
||||||
|
|||||||
|
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,
|
malik,
|
||||||
bena,
|
bena,
|
||||||
john,
|
john,
|
||||||
leftIcon: require("./UI/leftIcon.png"),
|
|
||||||
rightIcon: require("./UI/rightIcon.png"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const videos = {
|
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,
|
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,14 +73,16 @@ const MusicLandHeader = ({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Image
|
{logo && (
|
||||||
source={logo}
|
<Image
|
||||||
style={{
|
source={logo}
|
||||||
alignSelf: "center",
|
style={{
|
||||||
height: isWeb ? 150 : 100,
|
alignSelf: "center",
|
||||||
resizeMode: "contain",
|
height: isWeb ? 150 : 100,
|
||||||
}}
|
resizeMode: "contain",
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
// Reanimated -> JS thread bridge
|
? Math.min(1, Math.max(0, offset.value / MAX_VALUE))
|
||||||
runOnJS(onSeek)(ratio);
|
: 0;
|
||||||
|
// Reanimated -> JS thread bridge
|
||||||
|
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)();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,20 +45,18 @@ 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,
|
ref: currentUID
|
||||||
} = useDataFromRef({
|
? projectsRef
|
||||||
ref: currentUID
|
.where("userId", "==", currentUID)
|
||||||
? projectsRef
|
.orderBy("updatedAt", "desc")
|
||||||
.where("userId", "==", currentUID)
|
: null,
|
||||||
.orderBy("updatedAt", "desc")
|
simpleRef: false,
|
||||||
: null,
|
listener: true,
|
||||||
simpleRef: false,
|
condition: !!currentUID,
|
||||||
listener: true,
|
refreshArray: [currentUID],
|
||||||
condition: !!currentUID,
|
});
|
||||||
refreshArray: [currentUID],
|
|
||||||
});
|
|
||||||
|
|
||||||
const userPlaybacks = Array.isArray(userProjects)
|
const userPlaybacks = Array.isArray(userProjects)
|
||||||
? userProjects.filter((project) => project.playbackUrl)
|
? userProjects.filter((project) => project.playbackUrl)
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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}…`;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
title: typeof project?.title === "string" ? project.title : "Sans titre",
|
{
|
||||||
artist: typeof project?.userName === "string" ? project.userName : "MusicLand",
|
id: project?.id
|
||||||
artwork: project?.coverUrl || null,
|
? `recorded-${project.id}`
|
||||||
coverUrl: project?.coverUrl || null,
|
: songUrl
|
||||||
metadata: { projectId: project?.id, screen: "RecordedPlayback" },
|
? `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
|
// 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,80 +175,151 @@ 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={{
|
||||||
|
alignItems: "none",
|
||||||
|
}}
|
||||||
|
backgroundImg={background.playbackBG2}
|
||||||
|
headerType="NONE"
|
||||||
|
>
|
||||||
|
<MusicLandHeader
|
||||||
|
progress={19}
|
||||||
|
onPressBack={goBack}
|
||||||
|
style={{
|
||||||
|
marginBottom: 40,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<View
|
<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 */}
|
||||||
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
|
{videoUri ? (
|
||||||
{videoUri ? (
|
<Pressable
|
||||||
<View
|
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={{
|
style={{
|
||||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
width: "100%",
|
||||||
maxWidth: "100%",
|
height: "100%",
|
||||||
aspectRatio: 9 / 16,
|
objectFit: "cover",
|
||||||
borderRadius: 12,
|
backgroundColor: "black",
|
||||||
backgroundColor: "#00000066",
|
|
||||||
overflow: "hidden",
|
|
||||||
alignSelf: "center",
|
|
||||||
position: "relative",
|
|
||||||
}}
|
}}
|
||||||
>
|
controls={false}
|
||||||
<video
|
/>
|
||||||
ref={videoElRef}
|
{!progress.playing && (
|
||||||
src={videoUri}
|
<View
|
||||||
muted
|
pointerEvents="none"
|
||||||
playsInline
|
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
position: "absolute",
|
||||||
height: "100%",
|
left: "50%",
|
||||||
objectFit: "cover",
|
top: "50%",
|
||||||
backgroundColor: "black",
|
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}
|
>
|
||||||
/>
|
<Image source={icons.play} style={{ width: 26, height: 26 }} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
)}
|
||||||
// Placeholder quand pas de vidéo sur web
|
</Pressable>
|
||||||
<View
|
) : (
|
||||||
style={{
|
// Placeholder quand pas de vidéo sur web
|
||||||
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
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
width: isWeb ? WEB_PREVIEW_WIDTH : "80%",
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
|
aspectRatio: 9 / 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: "#00000066",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
overflow: "hidden",
|
||||||
alignSelf: "center",
|
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
|
<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,36 +331,36 @@ 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
|
||||||
|
title="Je valide"
|
||||||
|
onPress={() => {
|
||||||
|
navigate(Routes.DownloadSongs, {
|
||||||
|
action: "playback",
|
||||||
|
uri: videoUri || null, // peut être null sur web
|
||||||
|
project,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<GradientButton
|
<BorderGradientButton
|
||||||
title="Je valide"
|
title="Recommencer"
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
navigate(Routes.DownloadSongs, {
|
try {
|
||||||
action: "playback",
|
await stopPlayback();
|
||||||
uri: videoUri || null, // peut être null sur web
|
} catch {}
|
||||||
project,
|
navigate(Routes.RecordPlayback, { project });
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
<BorderGradientButton
|
|
||||||
title="Recommencer"
|
|
||||||
onPress={async () => {
|
|
||||||
try {
|
|
||||||
await stopPlayback();
|
|
||||||
} catch {}
|
|
||||||
navigate(Routes.RecordPlayback, { project });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,16 +366,23 @@ const Playbacks = () => {
|
|||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
/>
|
/>
|
||||||
{showIndicators && (
|
{showIndicators && (
|
||||||
<View style={styles.indicatorContainer} pointerEvents="none">
|
<View style={styles.indicatorContainer} pointerEvents="box-none">
|
||||||
<Image
|
<Pressable
|
||||||
source={icons.chevronDown}
|
onPress={() => scrollToPlayback(-1)}
|
||||||
resizeMode="contain"
|
hitSlop={12}
|
||||||
style={[
|
disabled={!canScrollUp}
|
||||||
styles.indicatorArrow,
|
style={styles.indicatorArrowTouch}
|
||||||
styles.indicatorArrowUp,
|
>
|
||||||
!canScrollUp && styles.indicatorArrowDisabled,
|
<Image
|
||||||
]}
|
source={icons.chevronDown}
|
||||||
/>
|
resizeMode="contain"
|
||||||
|
style={[
|
||||||
|
styles.indicatorArrow,
|
||||||
|
styles.indicatorArrowUp,
|
||||||
|
!canScrollUp && styles.indicatorArrowDisabled,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
<View style={styles.indicatorDotsWrapper}>
|
<View style={styles.indicatorDotsWrapper}>
|
||||||
{indicatorItems.map((item, index) => (
|
{indicatorItems.map((item, index) => (
|
||||||
<View
|
<View
|
||||||
@@ -357,14 +406,21 @@ const Playbacks = () => {
|
|||||||
{/* {indicatorLabel ? (
|
{/* {indicatorLabel ? (
|
||||||
<Text style={styles.indicatorLabel}>{indicatorLabel}</Text>
|
<Text style={styles.indicatorLabel}>{indicatorLabel}</Text>
|
||||||
) : null} */}
|
) : null} */}
|
||||||
<Image
|
<Pressable
|
||||||
source={icons.chevronDown}
|
onPress={() => scrollToPlayback(1)}
|
||||||
resizeMode="contain"
|
hitSlop={12}
|
||||||
style={[
|
disabled={!canScrollDown}
|
||||||
styles.indicatorArrow,
|
style={styles.indicatorArrowTouch}
|
||||||
!canScrollDown && styles.indicatorArrowDisabled,
|
>
|
||||||
]}
|
<Image
|
||||||
/>
|
source={icons.chevronDown}
|
||||||
|
resizeMode="contain"
|
||||||
|
style={[
|
||||||
|
styles.indicatorArrow,
|
||||||
|
!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" }],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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,91 +142,36 @@ 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 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>
|
</View>
|
||||||
</CreateLyricsHeader>
|
</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>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||