clear more ticket, sub and design

This commit is contained in:
Thomas Demirdjian
2025-12-10 15:39:32 +01:00
parent b1ab6a549f
commit aacbd5411e
15 changed files with 1001 additions and 145 deletions
+99 -5
View File
@@ -1,6 +1,6 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, TextInput, View } from "react-native";
import React, { useCallback, useEffect, useRef } from "react";
import { Image, Pressable, TextInput } from "react-native";
import { icons } from "../assets";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
@@ -10,8 +10,99 @@ const SearchBar = ({
placeholder = "Que souhaites-tu écouter?",
textInputProps = {},
}) => {
const inputRef = useRef(null);
const hasForwardedFocusRef = useRef(false);
const focusFallbackRef = useRef(null);
const {
onFocus: onFocusProp,
onPressIn: onPressInProp,
ref: textInputRefProp,
...restTextInputProps
} = textInputProps;
const forwardFocus = useCallback(
(event) => {
if (hasForwardedFocusRef.current) return;
hasForwardedFocusRef.current = true;
onFocusProp?.(event);
},
[onFocusProp],
);
const handleFocus = useCallback(
(event) => {
forwardFocus(event);
},
[forwardFocus],
);
const focusInput = useCallback(() => {
const node = inputRef.current;
if (!node) {
return;
}
if (
typeof node.isFocused === "function" &&
node.isFocused() &&
hasForwardedFocusRef.current
) {
return;
}
hasForwardedFocusRef.current = false;
node.focus?.();
if (focusFallbackRef.current) {
cancelAnimationFrame(focusFallbackRef.current);
}
focusFallbackRef.current = requestAnimationFrame(() => {
// Some platforms do not propagate the focus event when focus() is called programmatically.
if (!hasForwardedFocusRef.current) {
forwardFocus();
}
if (typeof node.isFocused === "function" && !node.isFocused()) {
node.focus?.();
}
});
}, [forwardFocus]);
useEffect(() => {
return () => {
if (focusFallbackRef.current) {
cancelAnimationFrame(focusFallbackRef.current);
}
};
}, []);
const setRefs = useCallback(
(node) => {
inputRef.current = node;
if (typeof textInputRefProp === "function") {
textInputRefProp(node);
} else if (textInputRefProp) {
textInputRefProp.current = node;
}
},
[textInputRefProp],
);
const handleInputPressIn = useCallback(
(event) => {
focusInput();
onPressInProp?.(event);
},
[focusInput, onPressInProp],
);
return (
<View style={{ borderRadius: 12, overflow: "hidden" }}>
<Pressable
onPressIn={focusInput}
style={{ borderRadius: 12, overflow: "hidden" }}
>
<BlurView
intensity={80}
style={{
@@ -24,6 +115,7 @@ const SearchBar = ({
>
<Image source={icons.search} />
<TextInput
ref={setRefs}
placeholder={placeholder}
placeholderTextColor={Palette.gray}
style={{
@@ -32,10 +124,12 @@ const SearchBar = ({
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
flex: 1,
}}
{...textInputProps}
onPressIn={handleInputPressIn}
onFocus={handleFocus}
{...restTextInputProps}
/>
</BlurView>
</View>
</Pressable>
);
};
+32
View File
@@ -0,0 +1,32 @@
import React from "react";
import { Image } from "react-native";
import { subBadges } from "../assets";
const allowedLevels = new Set(["starter", "pro", "premium"]);
const normalizeLevel = (value) => {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim().toLowerCase();
return allowedLevels.has(normalized) ? normalized : null;
};
const SubscriptionBadge = ({ level = null, size = 20, style = null }) => {
const normalized = normalizeLevel(level);
const source = normalized ? subBadges[normalized] : null;
if (!source) {
return null;
}
return (
<Image
source={source}
style={[{ width: size, height: size }, style]}
resizeMode="contain"
/>
);
};
export default React.memo(SubscriptionBadge);
+6 -20
View File
@@ -18,7 +18,7 @@ import { isWeb } from "../../hooks/useLayoutType";
import CreditAmount from "../CreditAmount";
import { useStripe } from "../../providers/StripeProvider";
const WEB_MODAL_MAX_WIDTH = 1000;
const WEB_MODAL_MAX_WIDTH = 820;
const formatCurrency = (amount, currency = "eur") => {
if (typeof amount !== "number") {
return null;
@@ -229,14 +229,6 @@ function CoinPackCard({
) : null}
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
</View>
<View style={styles.perksList}>
<Text style={styles.perkItem}>
- Diffusion sur mes plates formes de streaming
</Text>
<Text style={styles.perkItem}>
*Eligible au Hit parade Chanson/Video
</Text>
</View>
<View style={styles.priceBlock}>
{formattedPrice ? (
<Text style={styles.packPrice}>{formattedPrice}</Text>
@@ -388,6 +380,9 @@ const CoinPackModal = ({ visible, onClose }) => {
width: "100%",
maxWidth: modalMaxWidth,
alignSelf: "center",
borderWidth: isWeb ? 1 : 0,
borderColor: "rgba(255,255,255,0.16)",
backgroundColor: isWeb ? "rgba(12, 10, 18, 0.85)" : undefined,
}}
>
<View style={styles.container}>
@@ -452,6 +447,7 @@ const styles = StyleSheet.create({
gap: 24,
alignItems: "center",
paddingBottom: gutters,
paddingHorizontal: isWeb ? gutters * 1.5 : 0,
width: "100%",
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : undefined,
},
@@ -503,7 +499,7 @@ const styles = StyleSheet.create({
maxWidth: isWeb ? WEB_MODAL_MAX_WIDTH : "100%",
alignSelf: "center",
gap: gutters,
paddingHorizontal: isWeb ? 8 : 0,
paddingHorizontal: isWeb ? gutters * 1.5 : 0,
},
packListWeb: {
flexDirection: "row",
@@ -567,16 +563,6 @@ const styles = StyleSheet.create({
color: "rgba(255, 255, 255, 0.72)",
textAlign: "center",
},
perksList: {
gap: 6,
alignItems: "center",
},
perkItem: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
color: "rgba(255, 255, 255, 0.72)",
textAlign: "center",
},
priceBlock: {
gap: 2,
alignItems: "center",
+340 -8
View File
@@ -1,10 +1,13 @@
import { useRoute } from "@react-navigation/native";
import React, { useMemo, useState } from "react";
import { Text, View } from "react-native";
import React, { useState } from "react";
import { Modal, Pressable, ScrollView, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background } from "../assets";
import BorderGradientButton from "../components/BorderGradientButton";
import GradientButton from "../components/GradientButton";
import { Input } from "../components/Input";
import AppCheckbox from "../components/AppCheckbox";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import {
checkIfPasswordIsStrongEnough,
@@ -30,10 +33,11 @@ const CreatePassword = () => {
const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState("");
const [loading, setLoading] = useState(false);
const [isCguAccepted, setIsCguAccepted] = useState(false);
const [isCguModalVisible, setIsCguModalVisible] = useState(false);
const [hasScrolledCguToEnd, setHasScrolledCguToEnd] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const handlePasswordChange = (text) => {
setPassword(text);
if (!text) {
@@ -52,6 +56,30 @@ const CreatePassword = () => {
const isPasswordValid = checkIfPasswordIsStrongEnough({ password });
const handleOpenCguModal = () => {
setHasScrolledCguToEnd(false);
setIsCguModalVisible(true);
};
const handleCloseCguModal = () => {
setIsCguModalVisible(false);
setHasScrolledCguToEnd(false);
};
const handleCguScroll = ({ nativeEvent }) => {
const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
const paddingToBottom = 20;
const contentFits = contentSize.height <= layoutMeasurement.height + 1;
const isAtEnd =
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom;
if (isAtEnd) {
setHasScrolledCguToEnd(true);
} else if (contentFits) {
setHasScrolledCguToEnd(true);
}
};
const onCreateAccount = async () => {
try {
if (!isPasswordValid) {
@@ -62,6 +90,14 @@ const CreatePassword = () => {
return;
}
if (!isCguAccepted) {
setTooltip({
text: "Tu dois accepter les CGU pour continuer",
type: "error",
});
return;
}
setLoading(true);
const cred = await firebase
.auth()
@@ -110,8 +146,19 @@ const CreatePassword = () => {
headerType="NAVIGATION"
title="Inscription"
>
<View style={{ flex: 1, paddingTop: 20 }}>
<ItemContainer height={300} disableKeyboardHeight>
<View
style={{
flex: 1,
paddingVertical: 20,
alignItems: "center",
justifyContent: "center",
}}
>
<ItemContainer
height={isWeb ? 360 : responsiveHeight(65)}
disableKeyboardHeight
style={{ width: "100%" }}
>
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
<View style={{ gap: 16 }}>
<View style={{ gap: 2 }}>
@@ -154,18 +201,303 @@ const CreatePassword = () => {
</Text>
) : null}
</View>
<View style={{ gap: 12 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 8,
}}
>
<AppCheckbox selected={isCguAccepted} />
<Pressable onPress={handleOpenCguModal}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textDecorationLine: "underline",
}}
>
Voir les CGU
</Text>
</Pressable>
</View>
</View>
<GradientButton
title={loading ? "Création..." : "Suivant"}
title={loading ? "Création..." : "Créer mon compte"}
containerStyle={{
width: "80%",
alignSelf: "center",
}}
onPress={onCreateAccount}
disabled={loading || !email || !isPasswordValid}
disabled={
loading || !email || !isPasswordValid || !isCguAccepted
}
/>
</View>
</ItemContainer>
</View>
<Modal
animationType="slide"
visible={isCguModalVisible}
transparent
onRequestClose={handleCloseCguModal}
>
<View
style={{
flex: 1,
backgroundColor: Palette.transparentBlack,
justifyContent: "center",
paddingHorizontal: 20,
paddingVertical: 40,
alignItems: "center",
}}
>
<View
style={{
backgroundColor: Palette.darkPurple,
borderRadius: 16,
padding: 20,
paddingTop: 28,
maxHeight: "75%",
width: "90%",
flexShrink: 1,
}}
>
<Pressable
onPress={handleCloseCguModal}
style={{
position: "absolute",
top: 12,
right: 12,
padding: 6,
zIndex: 2,
}}
>
<Text
style={{
fontSize: 16,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
X
</Text>
</Pressable>
<View style={{ gap: 16, flex: 1 }}>
<Text
style={{
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
}}
>
Conditions Générales d&apos;Utilisation
</Text>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ gap: 16, paddingBottom: 12 }}
onScroll={handleCguScroll}
onMomentumScrollEnd={handleCguScroll}
scrollEventThrottle={16}
style={{ flex: 1 }}
>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Cupidatat incididunt aute aute velit deserunt labore excepteur
eu velit cillum est. Laboris in sunt ea Lorem culpa velit nisi
ad elit esse voluptate id adipisicing nulla magna. Duis qui
fugiat consequat elit velit aute ad. Reprehenderit proident
duis exercitation velit proident aute minim. Anim
reprehenderit mollit consectetur id sint adipisicing pariatur
tempor sit pariatur proident nostrud. Sit culpa tempor enim ut
consectetur sunt aute est reprehenderit incididunt incididunt
in ullamco consectetur. Eiusmod non incididunt proident
eiusmod. Esse Lorem ut amet in est id aute consectetur.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Aliqua est sint consectetur occaecat exercitation. Elit nisi
velit ad qui eiusmod reprehenderit proident labore nostrud
labore consequat enim fugiat non. Eiusmod occaecat pariatur
deserunt velit elit sint irure fugiat excepteur labore ad
velit ex deserunt laborum. Aliqua excepteur reprehenderit
nostrud aliqua pariatur aliqua excepteur Lorem consectetur. Ut
est culpa ullamco ipsum Lorem ullamco ea labore anim.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Et ut ex duis tempor amet aliquip cupidatat sit sit. Aliqua
consectetur amet duis fugiat nulla duis culpa exercitation
reprehenderit. Ea voluptate proident ad elit pariatur do.
Culpa incididunt deserunt dolor ut officia aliquip ut
occaecat eiusmod sint veniam. Deserunt consequat adipisicing
aliquip velit labore fugiat aute culpa id sint. Adipisicing eu
commodo ex do aliqua in labore laboris sit sunt adipisicing
excepteur. Do sint velit veniam pariatur consequat proident
cupidatat in incididunt ullamco.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Culpa eu cillum fugiat elit eiusmod enim. Cupidatat culpa
aliquip culpa et tempor est velit. Aute id deserunt non ut
minim deserunt adipisicing sit veniam eu id incididunt
adipisicing dolore. Eiusmod nisi excepteur est voluptate
consequat reprehenderit non exercitation commodo aute.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Eu culpa enim dolore anim ipsum eu veniam mollit dolore
officia cupidatat laborum officia do. Ad proident in anim nisi
exercitation consequat exercitation occaecat consequat ut
laborum esse consectetur ullamco. Enim elit sit in id velit
quis nostrud nostrud eu labore aute reprehenderit voluptate
deserunt. Minim voluptate et duis voluptate enim duis.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Minim laboris deserunt minim ex. Duis aliquip cillum proident.
Magna velit nisi fugiat. Non id proident pariatur elit in
exercitation et amet id qui ad laborum nulla ullamco ea.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Cillum nisi aute dolore culpa est veniam cupidatat sunt sint
ipsum. Amet ullamco minim non voluptate cillum dolore sunt
irure nulla pariatur excepteur voluptate id. Commodo ullamco
aliqua non ea mollit ullamco do minim dolor magna. Ipsum eu
minim quis laborum do dolore labore eiusmod et. Ullamco
officia velit anim. Exercitation voluptate reprehenderit ex et
do eu fugiat tempor do cillum ad.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Cupidatat consectetur excepteur commodo laborum incididunt
laboris minim dolore ut ipsum dolor ullamco culpa aliquip.
Cillum proident quis consectetur voluptate labore nisi Lorem
pariatur in esse. Amet ipsum mollit officia fugiat Lorem ipsum
elit officia. Esse reprehenderit magna quis irure sit
consectetur dolore sunt mollit aliquip eiusmod voluptate amet.
Ex irure culpa ea cupidatat nulla ea labore aute occaecat
consequat consectetur cillum amet. Velit ad do occaecat non
elit quis. Amet id reprehenderit ullamco amet tempor deserunt
exercitation elit consectetur minim aliqua. Cillum do aliquip
do ea ipsum veniam deserunt in ipsum pariatur nisi proident et
ut deserunt.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Mollit et adipisicing velit tempor deserunt excepteur fugiat
eiusmod adipisicing. Tempor do ex duis dolor deserunt cillum
officia est nisi mollit fugiat. Amet duis ea laboris officia
aliquip id sint voluptate consectetur velit elit
reprehenderit nostrud exercitation. Ea dolore adipisicing nulla
incididunt laboris sint commodo non mollit eiusmod. Tempor
nulla eu laborum tempor veniam laboris consequat non consequat
exercitation pariatur velit. Voluptate magna mollit esse
incididunt id. Pariatur eu irure esse ullamco fugiat culpa.
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
lineHeight: 20,
}}
>
Sint dolore aliqua pariatur do mollit occaecat deserunt qui
proident non exercitation mollit cillum culpa. Est cupidatat
consequat ea commodo laborum labore sunt excepteur labore aute
est amet anim. Tempor deserunt labore mollit enim officia
aliqua occaecat. Ullamco mollit qui mollit do ex irure enim.
</Text>
</ScrollView>
{hasScrolledCguToEnd ? (
<View style={{ gap: 10 }}>
<GradientButton
title="Accepter sans réserve"
onPress={() => {
setIsCguAccepted(true);
handleCloseCguModal();
}}
/>
<BorderGradientButton
title="Refuser"
onPress={handleCloseCguModal}
/>
</View>
) : (
<Text
style={{
fontSize: 12,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
}}
>
Fais défiler jusqu&apos;en bas pour afficher les actions
</Text>
)}
</View>
</View>
</View>
</Modal>
</Page>
);
};
+120 -2
View File
@@ -21,7 +21,7 @@ import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import SearchBar from "../../components/SearchBar";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import { projectsRef } from "../../config/firebase";
import firebase, { projectsRef, usersRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import useSearch from "../../hooks/useSearch";
@@ -37,6 +37,25 @@ import SearchResultsList from "../Library/components/SearchResultsList";
import PlaybacksCard from "./components/PlaybacksCard";
import SongCard from "./components/SongCard";
import BorderGradient from "../../components/BorderGradient/BorderGradient.web";
const allowedPremiumLevels = new Set(["starter", "pro", "premium"]);
const normalizePremiumLevel = (value) => {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim().toLowerCase();
return allowedPremiumLevels.has(normalized) ? normalized : null;
};
const chunkArray = (items = [], size = 10) => {
const chunks = [];
for (let i = 0; i < items.length; i += size) {
chunks.push(items.slice(i, i + size));
}
return chunks;
};
const HitParade = () => {
const [selectedCategory, setSelectedCategory] = useState("Chansons");
const navigateToMusicDetails = useNavigateToMusicDetails();
@@ -92,6 +111,7 @@ const HitParade = () => {
title={item?.title || "Sans titre"}
artist={item?.userName}
coverUrl={item?.coverUrl || null}
subscriptionLevel={getCreatorLevel(item)}
onPress={() =>
navigateToMusicDetails({
projectId: item.id,
@@ -118,6 +138,7 @@ const HitParade = () => {
title={item?.title || "Sans titre"}
artist={item?.userName}
thumbnailUrl={resolvePlaybackThumbnail(item)}
subscriptionLevel={getCreatorLevel(item)}
onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}
/>
)}
@@ -133,6 +154,7 @@ const HitParade = () => {
title={item?.title || "Sans titre"}
artist={item?.userName}
coverUrl={item?.coverUrl || null}
subscriptionLevel={getCreatorLevel(item)}
onPress={() =>
navigateToMusicDetails({
projectId: item.id,
@@ -154,6 +176,7 @@ const HitParade = () => {
title={item?.title || "Sans titre"}
artist={item?.userName}
thumbnailUrl={resolvePlaybackThumbnail(item)}
subscriptionLevel={getCreatorLevel(item)}
onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}
/>
))}
@@ -188,6 +211,99 @@ const HitParade = () => {
[musics]
);
const [creatorsById, setCreatorsById] = useState({});
const requiredUserIds = useMemo(() => {
const collected = new Set();
const collectUserId = (project) => {
const uid =
project && typeof project.userId === "string"
? project.userId.trim()
: null;
if (uid) {
collected.add(uid);
}
};
songsList.forEach(collectUserId);
playbackList.forEach(collectUserId);
return Array.from(collected);
}, [playbackList, songsList]);
const fetchCreatorsByIds = useCallback(
async (userIds) => {
if (!Array.isArray(userIds) || userIds.length === 0) {
return;
}
const normalizedIds = userIds
.map((id) => (typeof id === "string" ? id.trim() : null))
.filter(Boolean);
if (normalizedIds.length === 0) {
return;
}
const pendingIds = new Set(normalizedIds);
const nextCreators = {};
const idChunks = chunkArray(normalizedIds, 10);
await Promise.all(
idChunks.map(async (chunk) => {
try {
const snapshot = await usersRef
.where(firebase.firestore.FieldPath.documentId(), "in", chunk)
.get();
snapshot.docs.forEach((doc) => {
const data = doc.data() || {};
nextCreators[doc.id] = {
premiumLevel: normalizePremiumLevel(data.premiumLevel),
};
pendingIds.delete(doc.id);
});
} catch (error) {
console.log(
"HitParade: unable to fetch creators",
error?.message || error
);
}
})
);
pendingIds.forEach((userId) => {
nextCreators[userId] = { premiumLevel: null };
});
if (Object.keys(nextCreators).length > 0) {
setCreatorsById((previous) => ({ ...previous, ...nextCreators }));
}
},
[setCreatorsById]
);
useEffect(() => {
const missingIds = requiredUserIds.filter((id) => !creatorsById[id]);
if (missingIds.length === 0) {
return;
}
fetchCreatorsByIds(missingIds);
}, [creatorsById, fetchCreatorsByIds, requiredUserIds]);
const getCreatorLevel = useCallback(
(project) => {
const uid =
project && typeof project.userId === "string"
? project.userId.trim()
: null;
if (!uid) {
return null;
}
return creatorsById?.[uid]?.premiumLevel || null;
},
[creatorsById]
);
const playbackResults = useMemo(
() => (Array.isArray(playbacks) ? playbacks : []),
[playbacks]
@@ -246,7 +362,7 @@ const HitParade = () => {
}, [closeDropdown, dropdownVisible, isWeb]);
const shouldShowResults = isWeb && dropdownVisible;
const shouldBlurContent = shouldShowResults && hasSearchQuery;
const shouldBlurContent = shouldShowResults;
const handlePlayRandomSong = useCallback(() => {
const arr = Array.isArray(topSongs) ? topSongs : [];
@@ -520,7 +636,9 @@ const HitParade = () => {
StyleSheet.absoluteFillObject,
{
zIndex: 10,
borderRadius: 18,
backgroundColor: "rgba(0, 0, 0, 0.25)",
overflow: "hidden",
},
]}
/>
@@ -4,6 +4,7 @@ import { BlurView } from "expo-blur";
import { img } from "../../../assets";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import SubscriptionBadge from "../../../components/SubscriptionBadge";
const PlaybacksCard = ({
rank = 1,
@@ -12,6 +13,7 @@ const PlaybacksCard = ({
thumbnailUrl = null,
coverUrl = null,
onPress = () => null,
subscriptionLevel = null,
}) => {
const resolveUri = (value) =>
typeof value === "string" && value.trim().length > 0 ? value : null;
@@ -38,43 +40,67 @@ const PlaybacksCard = ({
style={{
...Style.containerRow,
gap: 15,
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<Text
<View
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
...Style.containerRow,
gap: 15,
alignItems: "center",
flex: 1,
}}
>
{rank}
</Text>
<Image
source={imageUri ? { uri: imageUri } : img.placeholder3}
style={{ width: 67, height: 108, borderRadius: 16 }}
/>
<View>
<Text
style={{
fontSize: 16,
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
numberOfLines={1}
>
{title}
</Text>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
numberOfLines={1}
>
{artist}
{rank}
</Text>
<Image
source={imageUri ? { uri: imageUri } : img.placeholder3}
style={{ width: 67, height: 108, borderRadius: 16 }}
/>
<View style={{ flex: 1 }}>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
}}
numberOfLines={1}
>
{title}
</Text>
<View
style={{
...Style.containerRow,
gap: 6,
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
flexShrink: 1,
}}
numberOfLines={1}
>
{artist}
</Text>
</View>
</View>
</View>
<SubscriptionBadge level={subscriptionLevel} size={36} />
</View>
</BlurView>
</Pressable>
+25 -7
View File
@@ -12,6 +12,7 @@ import { BlurView } from "expo-blur";
import Style, { size } from "../../../styles/Style";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import SubscriptionBadge from "../../../components/SubscriptionBadge";
const SongCard = ({
rank = 1,
@@ -19,6 +20,7 @@ const SongCard = ({
artist = "MusicLand",
coverUrl = null,
onPress = null,
subscriptionLevel = null,
}) => {
return (
<Pressable
@@ -69,7 +71,7 @@ const SongCard = ({
>
{rank}
</Text>
<View>
<View style={{ flex: 1 }}>
<Text
style={{
fontSize: 16,
@@ -79,16 +81,32 @@ const SongCard = ({
>
{title}
</Text>
<Text
<View
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
...Style.containerRow,
gap: 6,
flexWrap: "wrap",
alignItems: "center",
justifyContent: "space-between",
}}
>
{artist}
</Text>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
flexShrink: 1,
}}
>
{artist}
</Text>
</View>
</View>
<SubscriptionBadge
level={subscriptionLevel}
size={36}
style={{ marginLeft: "auto" }}
/>
</View>
</BlurView>
</Pressable>
+3 -2
View File
@@ -104,7 +104,7 @@ const Library = () => {
};
const shouldShowResults = dropdownVisible;
const shouldBlurContent = dropdownVisible && hasSearchQuery;
const shouldBlurContent = dropdownVisible;
const handleChangeText = (value) => {
setSearch(value);
@@ -214,8 +214,9 @@ const Library = () => {
StyleSheet.absoluteFillObject,
{
zIndex: 10,
borderRadius: 0,
borderRadius: 18,
backgroundColor: "rgba(0, 0, 0, 0.25)",
overflow: "hidden",
},
]}
/>
+39 -14
View File
@@ -2,16 +2,15 @@ import { useIsFocused } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import moment from "moment";
import React, { useEffect, useMemo, 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 { responsiveHeight } from "react-native-responsive-dimensions";
import { ai, background } from "../../assets";
import { background } from "../../assets";
import AppAlert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import ProgressBar from "../../components/ProgressBar";
import firebase, { projectsRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
@@ -23,6 +22,7 @@ import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
const GeneratingSong = () => {
const { selectedProjectId, selectedProject } = useUser();
const [progress, setProgress] = useState(0);
const [isGenerationInFlight, setIsGenerationInFlight] = useState(false);
const { setIsLoading } = useMinuit();
const progressTimerRef = useRef(null);
const navigatedRef = useRef(false);
@@ -59,21 +59,29 @@ const GeneratingSong = () => {
? selectedProject.musicUrls.filter(Boolean)
: [];
const hasReadySong = readyUrls.length > 0 || !!selectedProject?.songUrl;
const isActivelyGenerating =
status === "GENERATING" ||
(isGenerationInFlight &&
status !== "FAILED" &&
status !== "GENERATED");
if (status !== "GENERATING") {
if (!isActivelyGenerating) {
// Reset local start when leaving generating state
localStartRef.current = null;
}
if (status === "GENERATED") {
clearTimer();
setIsGenerationInFlight(false);
setProgress(hasReadySong ? 100 : maxGeneratingProgress);
return () => clearTimer();
}
if (status !== "GENERATING") {
if (!isActivelyGenerating) {
clearTimer();
setProgress(0);
if (status !== "FAILED") {
setProgress(0);
}
return () => clearTimer();
}
@@ -81,11 +89,8 @@ const GeneratingSong = () => {
const gs = selectedProject?.generationStartAt;
if (gs?.toDate) return gs.toDate();
if (gs) return new Date(gs);
if (status === "GENERATING") {
if (!localStartRef.current) localStartRef.current = new Date();
return localStartRef.current;
}
return null;
if (!localStartRef.current) localStartRef.current = new Date();
return localStartRef.current;
};
const update = () => {
@@ -97,7 +102,7 @@ const GeneratingSong = () => {
const elapsed = moment().diff(moment(startDate));
const raw = Math.floor((elapsed / totalMs) * 100);
// While status is GENERATING, block visual progress at 90%
const pct = Math.max(0, Math.min(maxGeneratingProgress, raw));
const pct = Math.max(1, Math.min(maxGeneratingProgress, raw));
setProgress(pct);
};
@@ -106,6 +111,7 @@ const GeneratingSong = () => {
progressTimerRef.current = global.setInterval(update, 1000);
return () => clearTimer();
}, [
isGenerationInFlight,
selectedProject?.musicStatus,
selectedProject?.generationStartAt,
selectedProject?.musicUrls,
@@ -121,6 +127,15 @@ const GeneratingSong = () => {
}
}, [selectedProject?.musicStatus, selectedProjectId]);
useEffect(() => {
if (
selectedProject?.musicStatus === "GENERATED" ||
selectedProject?.musicStatus === "FAILED"
) {
setIsGenerationInFlight(false);
}
}, [selectedProject?.musicStatus]);
const effectiveConfig = useMemo(() => {
// Use selectedProject.musicConfig only
const cfg = selectedProject?.musicConfig || {};
@@ -137,6 +152,11 @@ const GeneratingSong = () => {
async function startMusicGeneration() {
try {
console.log("startMusicGeneration");
setIsGenerationInFlight(true);
if (!localStartRef.current) {
localStartRef.current = new Date();
}
setProgress(1);
await setIsLoading(true);
const callable = firebase
.functions()
@@ -216,6 +236,7 @@ const GeneratingSong = () => {
}
}
askedRef.current = false;
setIsGenerationInFlight(false);
} finally {
await setIsLoading(false);
}
@@ -279,7 +300,11 @@ const GeneratingSong = () => {
]);
const progressStatus = isFailed ? "error" : "default";
const progressLabel = isFailed ? "Erreur" : `${progress}%`;
const displayProgress = Math.max(
0,
Math.min(100, Math.round(progress || 0)),
);
const progressLabel = isFailed ? "Erreur" : `${displayProgress}%`;
return (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
@@ -338,7 +363,7 @@ const GeneratingSong = () => {
<View style={{ alignItems: "center", gap: 16 }}>
<ProgressBar
gradient
progress={progress}
progress={displayProgress}
status={progressStatus}
/>
<Text
+258 -56
View File
@@ -69,7 +69,7 @@ const getIntervalLabel = (recurring) => {
return `tous les ${count} ${count > 1 ? terms.plural : terms.singular}`;
};
function SubscriptionCard({ plan, selected, onSelect }) {
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
const planKey = getPlanKeyForBadge(plan);
const planName =
(planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) ||
@@ -87,7 +87,6 @@ function SubscriptionCard({ plan, selected, onSelect }) {
const planBadgeKey = planKey;
const planBadgeSource =
planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null;
const planFeatures = SUBSCRIPTION_FEATURES;
const handleSelect = React.useCallback(() => {
if (typeof onSelect === "function" && plan?.priceId) {
onSelect(plan.priceId);
@@ -156,22 +155,23 @@ function SubscriptionCard({ plan, selected, onSelect }) {
</View>
) : null}
{planFeatures?.length ? (
<View style={styles.features}>
{planFeatures.map((feature) => (
<Text key={feature} style={styles.featureText}>
{feature}
</Text>
))}
</View>
) : null}
</View>
</BlurView>
{isAnnual ? (
<View style={styles.annualBadge}>
<Text style={styles.annualBadgeText}>2 mois offert</Text>
</View>
) : null}
</Pressable>
);
}
const PRICE_PRIORITY_BY_PERIOD = {
monthly: [
"price_1SPgitCzf2o5bDRdbnhLFx6f", // Starter
"price_1SPgjCCzf2o5bDRdr08Xzp8u", // Pro
"price_1SPgjaCzf2o5bDRdd9Xo2u26", // Premium
],
annual: [
"price_1SPgkDCzf2o5bDRdNGLVNeQ3", // Starter
"price_1SPgkXCzf2o5bDRdejBVxEBY", // Pro
@@ -180,6 +180,11 @@ const PRICE_PRIORITY_BY_PERIOD = {
};
const PACK_PRICE_ID_BY_PERIOD = {
monthly: {
starter: "price_1SPgitCzf2o5bDRdbnhLFx6f",
pro: "price_1SPgjCCzf2o5bDRdr08Xzp8u",
premium: "price_1SPgjaCzf2o5bDRdd9Xo2u26",
},
annual: {
starter: "price_1SPgkDCzf2o5bDRdNGLVNeQ3",
pro: "price_1SPgkXCzf2o5bDRdejBVxEBY",
@@ -212,12 +217,6 @@ const PLAN_DISPLAY_NAME_BY_KEY = {
premium: "Play Backer Gold",
};
const SUBSCRIPTION_FEATURES = [
"*Eligible au Hit parade Vidéo",
"Privilège Membre : Récompense doublée",
"Eligible au hit parade Artiste trimestriel et annuel",
];
const getPlanKeyForBadge = (plan) => {
if (!plan || typeof plan !== "object") {
return null;
@@ -293,7 +292,7 @@ const HERO_IMAGE_HEIGHT = isWeb ? 760 : 360;
const PAGE_BACKGROUND_COLOR = "#303438";
const SUBSCRIPTION_DISCLAIMER =
"Résiliable en un clic à tout moment. Les prix sont indiqués TTC. Contacte-nous pour des besoins spécifiques (facturation annuelle, volume, offres éducation).";
const CARD_MIN_HEIGHT = isWeb ? 320 : 240;
const CARD_MIN_HEIGHT = isWeb ? 260 : 180;
export default function Subscriptions() {
const route = useRoute();
@@ -313,16 +312,29 @@ export default function Subscriptions() {
catalogError,
createSubscriptionCheckout,
} = useStripe();
const [selectedPeriodKey, setSelectedPeriodKey] = React.useState("annual");
const [selectedPriceId, setSelectedPriceId] = React.useState(null);
const [processingPriceId, setProcessingPriceId] = React.useState(null);
const [errorMessage, setErrorMessage] = React.useState(null);
const initialPackHandledRef = React.useRef(false);
const normalizedPlans = React.useMemo(
() => normalizePlans(subscriptions?.annual, "annual"),
[subscriptions?.annual]
const normalizedPlansByPeriod = React.useMemo(
() => ({
monthly: normalizePlans(subscriptions?.monthly, "monthly"),
annual: normalizePlans(subscriptions?.annual, "annual"),
}),
[subscriptions?.annual, subscriptions?.monthly]
);
const hasAnyPlan = (normalizedPlans?.length || 0) > 0;
const availablePeriods = React.useMemo(
() =>
["monthly", "annual"].filter(
(period) => (normalizedPlansByPeriod?.[period]?.length || 0) > 0
),
[normalizedPlansByPeriod]
);
const hasAnyPlan = availablePeriods.length > 0;
const isLoadingPlans = isCatalogLoading && !hasAnyPlan;
const combinedErrorMessage = errorMessage || catalogError;
@@ -331,25 +343,39 @@ export default function Subscriptions() {
}, [initialSubscriptionPack]);
React.useEffect(() => {
const periodEntries = Object.entries(normalizedPlansByPeriod).filter(
([, plans]) => (plans?.length || 0) > 0
);
if (!periodEntries.length) {
setSelectedPriceId(null);
return;
}
const shouldApplyPack =
Boolean(initialSubscriptionPack) && !initialPackHandledRef.current;
let matchedPriceId = null;
let matchedPeriodKey = selectedPeriodKey;
let nextPeriodKey =
periodEntries.find(([periodKey]) => periodKey === selectedPeriodKey)?.[0] ||
periodEntries[0]?.[0] ||
"annual";
if (shouldApplyPack && normalizedPlans?.length) {
if (shouldApplyPack) {
const desired = initialSubscriptionPack.trim().toLowerCase();
const candidateId = PACK_PRICE_ID_BY_PERIOD?.annual?.[desired];
if (candidateId) {
const exists = normalizedPlans.some(
(plan) => plan?.priceId === candidateId
);
if (exists) {
matchedPriceId = candidateId;
for (const [periodKey, plans] of periodEntries) {
const candidateId = PACK_PRICE_ID_BY_PERIOD?.[periodKey]?.[desired];
if (candidateId) {
const exists = plans.some((plan) => plan?.priceId === candidateId);
if (exists) {
matchedPriceId = candidateId;
matchedPeriodKey = periodKey;
break;
}
}
}
if (!matchedPriceId) {
const matched = normalizedPlans.find((plan) => {
const matched = plans.find((plan) => {
const label = (plan?.product?.name || plan?.nickname || "")
.toString()
.toLowerCase();
@@ -358,25 +384,38 @@ export default function Subscriptions() {
if (matched?.priceId) {
matchedPriceId = matched.priceId;
matchedPeriodKey = periodKey;
break;
}
}
}
setSelectedPriceId((current) => {
if (matchedPriceId) {
nextPeriodKey = matchedPeriodKey;
}
const plansForPeriod = normalizedPlansByPeriod?.[nextPeriodKey] || [];
const nextPriceId = (() => {
if (matchedPriceId) {
return matchedPriceId;
}
const hasCurrent = normalizedPlans?.some(
(plan) => plan.priceId === current
const hasCurrent = plansForPeriod.some(
(plan) => plan.priceId === selectedPriceId
);
if (hasCurrent) {
return current;
return selectedPriceId;
}
return plansForPeriod?.[0]?.priceId || null;
})();
return normalizedPlans?.[0]?.priceId || null;
});
if (nextPeriodKey !== selectedPeriodKey) {
setSelectedPeriodKey(nextPeriodKey);
}
if (nextPriceId !== selectedPriceId) {
setSelectedPriceId(nextPriceId);
}
if (matchedPriceId || (shouldApplyPack && !isCatalogLoading)) {
initialPackHandledRef.current = true;
@@ -384,10 +423,12 @@ export default function Subscriptions() {
}, [
initialSubscriptionPack,
isCatalogLoading,
normalizedPlans,
normalizedPlansByPeriod,
selectedPeriodKey,
selectedPriceId,
]);
const currentPlans = normalizedPlans || [];
const currentPlans = normalizedPlansByPeriod?.[selectedPeriodKey] || [];
const handleSelect = React.useCallback(
(priceId) => {
@@ -438,13 +479,36 @@ export default function Subscriptions() {
goBack();
}, []);
const handlePeriodChange = React.useCallback(
(periodKey) => {
if (!periodKey || periodKey === selectedPeriodKey) {
return;
}
setSelectedPeriodKey(periodKey);
setSelectedPriceId(null);
setProcessingPriceId(null);
},
[selectedPeriodKey]
);
const renderHeaderSection = React.useCallback(() => {
return (
<>
<View style={styles.header}>
<Text style={styles.title}>
Choisissez labonnement qui vous correspond
Rejoignez le club MusicLand
</Text>
<View style={styles.benefitsBox}>
<Text style={styles.benefitsTitle}>Privilège Membre :</Text>
<View style={styles.benefitsList}>
<Text style={styles.benefitsItem}>
- Eligible au concours mensuel chanson/Vidéo
</Text>
<Text style={styles.benefitsItem}>
- Crédits gratuits tous les mois
</Text>
</View>
</View>
</View>
{combinedErrorMessage ? (
<Text style={styles.errorText}>{combinedErrorMessage}</Text>
@@ -453,6 +517,66 @@ export default function Subscriptions() {
);
}, [combinedErrorMessage]);
const renderSegmentedControl = React.useCallback(() => {
const segments = [
{ key: "monthly", label: "Mensuel", plans: normalizedPlansByPeriod?.monthly },
{ key: "annual", label: "Annuel", plans: normalizedPlansByPeriod?.annual },
];
const visibleSegments = segments.filter(
(segment) => (segment.plans?.length || 0) > 0
);
if (visibleSegments.length <= 1) {
return null;
}
return (
<View
style={[
styles.segmentedControl,
isMobile && styles.segmentedControlMobile,
]}
>
{visibleSegments.map((segment) => {
const isActive = selectedPeriodKey === segment.key;
const showAnnualPromo = segment.key === "annual";
return (
<Pressable
key={segment.key}
accessibilityRole="button"
accessibilityState={{ selected: isActive }}
onPress={() => handlePeriodChange(segment.key)}
style={[
styles.segmentButton,
isActive && styles.segmentButtonActive,
]}
>
<Text
style={[
styles.segmentLabel,
isActive && styles.segmentLabelActive,
]}
>
{segment.label}
</Text>
{showAnnualPromo ? (
<View style={styles.segmentBadge}>
<Text style={styles.segmentBadgeText}>-16%</Text>
</View>
) : null}
</Pressable>
);
})}
</View>
);
}, [
handlePeriodChange,
isMobile,
normalizedPlansByPeriod,
selectedPeriodKey,
]);
const renderMobilePlanItem = React.useCallback(
({ item }) => (
<View style={styles.mobileCard}>
@@ -460,10 +584,11 @@ export default function Subscriptions() {
plan={item}
selected={selectedPriceId === item.priceId}
onSelect={handleSelect}
isAnnual={selectedPeriodKey === "annual"}
/>
</View>
),
[handleSelect, selectedPriceId]
[handleSelect, selectedPeriodKey, selectedPriceId]
);
const renderMobileEmptyComponent = React.useCallback(() => {
@@ -497,9 +622,10 @@ export default function Subscriptions() {
return (
<View style={styles.mobileStickyHeader}>
{renderHeaderSection()}
{renderSegmentedControl()}
</View>
);
}, [renderHeaderSection]);
}, [renderHeaderSection, renderSegmentedControl]);
const renderMobileBottomActions = React.useCallback(() => {
return (
@@ -588,6 +714,8 @@ export default function Subscriptions() {
<>
{renderHeaderSection()}
{renderSegmentedControl()}
{isLoadingPlans && currentPlanCount === 0 ? (
<View style={styles.loaderContainer}>
<ActivityIndicator color={Palette.white} />
@@ -607,6 +735,7 @@ export default function Subscriptions() {
plan={plan}
selected={selectedPriceId === plan.priceId}
onSelect={handleSelect}
isAnnual={selectedPeriodKey === "annual"}
/>
))}
</View>
@@ -700,6 +829,29 @@ const styles = StyleSheet.create({
gap: 12,
alignItems: "center",
},
benefitsBox: {
width: "100%",
gap: 6,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 16,
backgroundColor: "rgba(255, 255, 255, 0.08)",
},
benefitsTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 14,
color: Palette.white,
textAlign: "center",
},
benefitsList: {
gap: 2,
},
benefitsItem: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: "rgba(255, 255, 255, 0.85)",
textAlign: "center",
},
webBackContainer: {
alignSelf: "flex-start",
marginBottom: 12,
@@ -760,6 +912,61 @@ const styles = StyleSheet.create({
alignItems: "stretch",
justifyContent: "center",
},
segmentedControl: {
flexDirection: "row",
alignSelf: "center",
justifyContent: "center",
padding: 4,
borderRadius: 999,
backgroundColor: "rgba(255, 255, 255, 0.08)",
marginTop: isWeb ? 12 : 8,
marginBottom: isWeb ? 8 : 4,
},
segmentButton: {
paddingVertical: 8,
paddingHorizontal: 18,
borderRadius: 999,
position: "relative",
},
segmentButtonActive: {
backgroundColor: "rgba(255, 255, 255, 0.18)",
},
segmentLabel: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: "rgba(255, 255, 255, 0.7)",
},
segmentLabelActive: {
color: Palette.white,
},
segmentBadge: {
position: "absolute",
top: -6,
right: -8,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 999,
backgroundColor: Palette.red,
},
segmentBadgeText: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 11,
color: Palette.white,
},
annualBadge: {
position: "absolute",
top: isWeb ? 12 : 10,
right: isWeb ? 12 : 10,
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: Palette.red,
},
annualBadgeText: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 12,
color: Palette.white,
},
actions: {
width: "100%",
alignItems: "center",
@@ -772,6 +979,7 @@ const styles = StyleSheet.create({
width: "100%",
minWidth: 0,
minHeight: CARD_MIN_HEIGHT,
position: "relative",
borderRadius: 24,
overflow: "hidden",
borderWidth: 1,
@@ -854,16 +1062,6 @@ const styles = StyleSheet.create({
fontSize: 13,
color: Palette.primary,
},
features: {
marginTop: isWeb ? 12 : 8,
gap: 6,
},
featureText: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
lineHeight: 18,
color: "rgba(255, 255, 255, 0.85)",
},
priceValue: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: isWeb ? 22 : 20,
@@ -904,6 +1102,10 @@ const styles = StyleSheet.create({
backgroundColor: PAGE_BACKGROUND_COLOR,
alignItems: "center",
},
segmentedControlMobile: {
alignSelf: "center",
marginBottom: 0,
},
mobileList: {
flex: 1,
width: "100%",
+22 -2
View File
@@ -445,8 +445,17 @@ const PouchReady = () => {
Génération en cours.
</Text>
)}
<View style={{ alignItems: "center", gap: 10 }}>
<ProgressBar gradient progress={coverProgressValue} />
<View
style={[
styles.coverProgressWrapper,
isWeb ? styles.coverProgressWrapperWeb : null,
]}
>
<ProgressBar
gradient
progress={coverProgressValue}
containerStyle={styles.coverProgressBar}
/>
<Text
style={{
color: Palette.white,
@@ -824,6 +833,17 @@ const styles = StyleSheet.create({
gap: 12,
minWidth: 240,
},
coverProgressWrapper: {
alignItems: "center",
gap: 10,
width: 220,
},
coverProgressWrapperWeb: {
width: 260,
},
coverProgressBar: {
width: "100%",
},
customInputWrapper: {
alignSelf: "stretch",
borderRadius: 18,
+3 -1
View File
@@ -414,7 +414,9 @@ const SongDownload = ({ route }) => {
size={22}
color={Palette.white}
/>
<Text style={styles.downloadText}>Télécharger la musique</Text>
<Text style={styles.downloadText}>
Acheter ce morceau pour 1,99
</Text>
</Pressable>
</View>