clear lot of tickets

This commit is contained in:
Thomas Demirdjian
2025-11-07 17:33:06 +01:00
parent 4365f1e46d
commit d7d9211fbe
30 changed files with 970 additions and 355 deletions
+1
View File
@@ -72,6 +72,7 @@ const CreatePassword = () => {
await usersRef.doc(uid).set(
{
email: email.trim(),
emailNotifications: true,
...(trimmedFirstName ? { firstName: trimmedFirstName } : {}),
...(trimmedLastName ? { lastName: trimmedLastName } : {}),
...(trimmedCity ? { city: trimmedCity } : {}),
+63 -3
View File
@@ -89,6 +89,12 @@ const STAGE_CARD_CONTENT = [
];
const CLUB_CARD_IMAGE = icons.clubIcon;
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
"trialing",
"active",
"past_due",
"unpaid",
]);
const Home = ({ navigation, route }) => {
const {
@@ -102,6 +108,43 @@ const Home = ({ navigation, route }) => {
} = useUser();
const { setTooltip } = useMinuit();
const hasActiveSubscription = useMemo(() => {
if (!currentUserData) {
return false;
}
const pickStatus = (value) => {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed.toLowerCase() : null;
};
const statusCandidates = [
pickStatus(currentUserData?.stripeSubscriptionStatus),
pickStatus(currentUserData?.stripeSubscription?.status),
pickStatus(
currentUserData?.stripeSubscription?.stripeSubscriptionStatus,
),
pickStatus(currentUserData?.stripeSubscription?.metadata?.status),
].filter(Boolean);
if (
statusCandidates.some((status) =>
ACTIVE_SUBSCRIPTION_STATUSES.has(status),
)
) {
return true;
}
const hasPremiumLevel =
typeof currentUserData?.premiumLevel === "string" &&
currentUserData.premiumLevel.trim().length > 0;
return hasPremiumLevel;
}, [currentUserData]);
if (!currentUID) {
return <LandingPage />;
}
@@ -386,9 +429,17 @@ const Home = ({ navigation, route }) => {
const handleStagePress = useCallback(
(stageKey, isLocked) => {
if (!hasActiveProject || isLocked) {
if (isLocked) {
return;
}
if (!hasActiveProject || !currentProject) {
if (stageKey === "songwriter") {
handleStartNew();
}
return;
}
ensureProjectSelected();
const action = getStageAction(stageKey, currentProject);
if (!action?.route) {
@@ -396,7 +447,12 @@ const Home = ({ navigation, route }) => {
}
navigate(action.route, action.params);
},
[currentProject, ensureProjectSelected, hasActiveProject],
[
currentProject,
ensureProjectSelected,
hasActiveProject,
handleStartNew,
],
);
const handleClubPress = useCallback(() => {
@@ -464,7 +520,11 @@ const Home = ({ navigation, route }) => {
))}
</View>
<ClubCard image={CLUB_CARD_IMAGE} onPress={handleClubPress} />
<ClubCard
image={CLUB_CARD_IMAGE}
onPress={handleClubPress}
hasActiveSubscription={hasActiveSubscription}
/>
</View>
</Page>
</View>
-165
View File
@@ -1,165 +0,0 @@
import { BlurView } from "expo-blur";
import React, { useMemo, useState } from "react";
import { FlatList, Image, Platform, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, img } from "../../assets";
import alert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
import { projectsRef } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { getArtistDisplayName } from "../../utils/artistName";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import MusicCard from "../Library/components/MusicCard";
const HomeSave = () => {
const {
userProjects = [],
resetSelectedProject,
selectProject,
currentUserData,
} = useUser();
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []),
[userProjects]
);
const ownerDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, "MusicLand"),
[currentUserData]
);
const [, setTooltip] = useGlobal("_tooltip");
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [menuProjectId, setMenuProjectId] = useState(null);
return (
<Page backgroundImg={background.homeBG} headerType="NONE">
<View style={{ flex: 1 }}>
<Image
source={img.goodVibe}
style={{ alignSelf: "center", position: "absolute" }}
/>
{projects.length > 0 && (
<View
style={{
height: responsiveHeight(70),
paddingTop: responsiveHeight(6),
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
style={{
flex: 1,
borderRadius: 20,
overflow: "hidden",
backgroundColor: Palette.glass,
padding: 12,
gap: 8,
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
marginBottom: 8,
textAlign: "center",
}}
>
Musiques en cours
</Text>
<FlatList
data={projects}
keyExtractor={(item) => item.id}
contentContainerStyle={{ gap: 10, paddingBottom: 10 }}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={item?.userName || ownerDisplayName}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={getProjectLikes(item, LIKE_TARGET.SONG)}
onPress={() => {
selectProject(item.id);
navigate(Routes.Home);
}}
onPressMore={(posTop) => {
setMenuProjectId(item.id);
setMenuPosition(posTop);
setShowMenu(
(prev) =>
!prev || posTop?.top !== (menuPosition?.top ?? null)
);
}}
/>
)}
/>
<MoreMenu
visible={showMenu}
top={menuPosition?.top ?? 0}
position={menuPosition}
onClose={() => setShowMenu(false)}
inPlaylist={false}
projectId={menuProjectId}
extraItems={[
{
label: "Supprimer",
onPress: () =>
alert(
"Confirmer la suppression",
"Cette action supprimera définitivement ce projet.",
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
if (!menuProjectId) return;
await projectsRef.doc(menuProjectId).delete();
setTooltip({
type: "success",
text: "Projet supprimé",
});
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
}
},
},
],
{ cancelable: true }
),
},
]}
/>
</BlurView>
</View>
)}
<View style={{ paddingTop: 12, marginBottom: responsiveHeight(10) }}>
<GradientButton
title="Créer une nouvelle musique"
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => {
resetSelectedProject();
navigate(Routes.Home);
}}
/>
</View>
</View>
</Page>
);
};
export default Home;
+22 -16
View File
@@ -5,22 +5,28 @@ import { FONT_FAMILY } from "../../../styles/Fonts";
import { Palette } from "../../../styles";
import { icons } from "../../../assets";
const ClubCard = ({ image, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
>
<View style={styles.inner}>
<ExpoImage
source={icons.club}
contentFit="contain"
style={styles.clubLogo}
/>
<ExpoImage source={image} contentFit="contain" style={styles.image} />
<Text style={styles.subtitle}>Rejoins le club !</Text>
</View>
</Pressable>
);
const ClubCard = ({ image, onPress, hasActiveSubscription = false }) => {
const subtitle = hasActiveSubscription
? "Tu fais déjà partie du club !"
: "Rejoins le club !";
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
>
<View style={styles.inner}>
<ExpoImage
source={icons.club}
contentFit="contain"
style={styles.clubLogo}
/>
<ExpoImage source={image} contentFit="contain" style={styles.image} />
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
</Pressable>
);
};
export default memo(ClubCard);
+19 -2
View File
@@ -6,7 +6,7 @@ import React, {
useRef,
useState,
} from "react";
import { Image, Platform, ScrollView, Text, View } from "react-native";
import { Image, Platform, ScrollView, StyleSheet, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import SearchBar from "../../components/SearchBar";
@@ -91,6 +91,7 @@ const Library = () => {
const [dropdownVisible, setDropdownVisible] = useState(false);
const searchWrapperRef = useRef(null);
const hasSearchQuery = search.trim().length > 0;
const closeDropdown = useCallback(() => {
setDropdownVisible(false);
@@ -103,6 +104,7 @@ const Library = () => {
};
const shouldShowResults = dropdownVisible;
const shouldBlurContent = dropdownVisible && hasSearchQuery;
const handleChangeText = (value) => {
setSearch(value);
@@ -203,7 +205,21 @@ const Library = () => {
)}
</View>
</View>
<View style={{ flex: 1 }}>
<View style={{ flex: 1, position: "relative" }}>
{shouldBlurContent && (
<BlurView
intensity={35}
tint="dark"
style={[
StyleSheet.absoluteFillObject,
{
zIndex: 10,
borderRadius: 0,
backgroundColor: "rgba(0, 0, 0, 0.25)",
},
]}
/>
)}
<ScrollView
contentContainerStyle={{
flexGrow: 1,
@@ -212,6 +228,7 @@ const Library = () => {
paddingTop: Platform.OS !== "android" ? 20 : 0,
}}
showsVerticalScrollIndicator={false}
style={{ flex: 1, position: "relative", zIndex: 5 }}
>
<BlurView
intensity={40}
+37 -5
View File
@@ -221,6 +221,38 @@ const MusicDetails = ({ route }) => {
}
}, [project?.musicTimestamps, project?.songIndex]);
const estimatedDurationMs = useMemo(() => {
const toMs = (value) => {
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) return 0;
return num > 1000 ? Math.round(num) : Math.round(num * 1000);
};
const idx = Number(project?.songIndex);
const normalizedIdx = Number.isFinite(idx) && idx >= 0 ? idx : 0;
const tsEntry = project?.musicTimestamps?.[normalizedIdx];
if (tsEntry && typeof tsEntry === "object") {
const fromMetadata =
toMs(tsEntry.durationMs) ||
toMs(tsEntry.duration) ||
toMs(tsEntry.durationS) ||
toMs(tsEntry.durationSeconds) ||
toMs(tsEntry.audioDuration) ||
toMs(tsEntry.audioLength);
if (fromMetadata > 0) return fromMetadata;
}
let maxEndS = 0;
for (let i = 0; i < alignedWords.length; i++) {
const word = alignedWords[i];
const end = Number(word?.endS ?? word?.startS ?? 0);
if (Number.isFinite(end) && end > maxEndS) {
maxEndS = end;
}
}
return maxEndS > 0 ? Math.round(maxEndS * 1000) : 0;
}, [alignedWords, project?.musicTimestamps, project?.songIndex]);
const sliderDurationMs = durationMs > 0 ? durationMs : estimatedDurationMs;
// Reset counters when the track changes
useEffect(() => {
listenedMsRef.current = 0;
@@ -320,7 +352,7 @@ const MusicDetails = ({ route }) => {
const handleSliderSeek = useCallback(
async (ratio) => {
const dur = durationMs || 0;
const dur = sliderDurationMs || 0;
if (!trackDescriptor || dur <= 0) return;
const targetMs = Math.max(0, Math.floor(dur * ratio));
try {
@@ -333,7 +365,7 @@ const MusicDetails = ({ route }) => {
console.log("MusicDetails seek error", e?.message);
}
},
[durationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo]
[sliderDurationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo]
);
const handleToggleLoop = useCallback(async () => {
@@ -839,10 +871,10 @@ const MusicDetails = ({ route }) => {
<View style={{ paddingTop: 22 }}>
<Slider
value={fmt(positionMs)}
maxValue={fmt(durationMs)}
maxValue={fmt(sliderDurationMs)}
progress={
durationMs
? Math.min(1, Math.max(0, (positionMs || 0) / durationMs))
sliderDurationMs
? Math.min(1, Math.max(0, (positionMs || 0) / sliderDurationMs))
: 0
}
seekEnabled={!!songUrl}
+91 -4
View File
@@ -11,6 +11,7 @@ import React, {
import {
Pressable,
Image as RNImage,
Platform,
ScrollView,
StyleSheet,
Text,
@@ -52,6 +53,7 @@ import {
createMusicSharePayload,
openShareSheet,
} from "../../utils/shareSheet";
import { goBack } from "../../navigation/NavigationService";
// 20 secondes
const timeBeforeIncrement = 20000;
@@ -71,6 +73,29 @@ const MusicDetails = ({ route }) => {
const timerRef = React.useRef(null);
const hasAutoPlayedRef = React.useRef(false);
const { isLooping, setLooping } = usePlayer() || {};
const isWeb = Platform.OS === "web";
const handleBackPress = useCallback(() => {
goBack();
}, []);
const renderWebBackButton = useCallback(() => {
if (!isWeb) return null;
return (
<View style={styles.webBackContainer}>
<Pressable
accessibilityRole="button"
onPress={handleBackPress}
style={styles.webBackButton}
>
<RNImage
source={icons.chevronDown}
style={styles.webBackIcon}
resizeMode="contain"
/>
<Text style={styles.webBackText}>Retour</Text>
</Pressable>
</View>
);
}, [handleBackPress, isWeb]);
const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null,
@@ -297,6 +322,38 @@ const MusicDetails = ({ route }) => {
}
}, [project?.musicTimestamps, project?.songIndex]);
const estimatedDurationMs = useMemo(() => {
const toMs = (value) => {
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) return 0;
return num > 1000 ? Math.round(num) : Math.round(num * 1000);
};
const idx = Number(project?.songIndex);
const normalizedIdx = Number.isFinite(idx) && idx >= 0 ? idx : 0;
const tsEntry = project?.musicTimestamps?.[normalizedIdx];
if (tsEntry && typeof tsEntry === "object") {
const fromMetadata =
toMs(tsEntry.durationMs) ||
toMs(tsEntry.duration) ||
toMs(tsEntry.durationS) ||
toMs(tsEntry.durationSeconds) ||
toMs(tsEntry.audioDuration) ||
toMs(tsEntry.audioLength);
if (fromMetadata > 0) return fromMetadata;
}
let maxEndS = 0;
for (let i = 0; i < alignedWords.length; i++) {
const word = alignedWords[i];
const end = Number(word?.endS ?? word?.startS ?? 0);
if (Number.isFinite(end) && end > maxEndS) {
maxEndS = end;
}
}
return maxEndS > 0 ? Math.round(maxEndS * 1000) : 0;
}, [alignedWords, project?.musicTimestamps, project?.songIndex]);
const sliderDurationMs = durationMs > 0 ? durationMs : estimatedDurationMs;
// Reset counters when the track changes
useEffect(() => {
listenedMsRef.current = 0;
@@ -411,7 +468,7 @@ const MusicDetails = ({ route }) => {
const handleSliderSeek = useCallback(
async (ratio) => {
const dur = durationMs || 0;
const dur = sliderDurationMs || 0;
if (!trackDescriptor || dur <= 0) return;
const targetMs = Math.max(0, Math.floor(dur * ratio));
lastSeekTargetMs.current = targetMs;
@@ -431,7 +488,7 @@ const MusicDetails = ({ route }) => {
console.log("MusicDetails seek error", e?.message);
}
},
[trackDescriptor, durationMs, isCurrentTrack, ensureLoaded, seekTrackTo]
[trackDescriptor, sliderDurationMs, isCurrentTrack, ensureLoaded, seekTrackTo]
);
const handleToggleLoop = useCallback(async () => {
@@ -905,6 +962,8 @@ const MusicDetails = ({ route }) => {
return (
<Page
headerType="NAVIGATION"
hideBackButton={isWeb}
topStickyContent={renderWebBackButton}
title={action === "userProfile" ? "Mon profil" : "Détail musique"}
backgroundImg={
action === "userProfile" ? background.profileBG : background.libraryBG2
@@ -1078,8 +1137,10 @@ const MusicDetails = ({ route }) => {
</View>
<Slider
value={fmt(positionMs)}
maxValue={fmt(durationMs)}
progress={durationMs ? (positionMs || 0) / durationMs : 0}
maxValue={fmt(sliderDurationMs)}
progress={
sliderDurationMs ? (positionMs || 0) / sliderDurationMs : 0
}
seekEnabled={!!songUrl}
onSeekStart={handleSliderSeekStart}
onSeek={handleSliderSeek}
@@ -1191,6 +1252,32 @@ const MusicDetails = ({ route }) => {
export default MusicDetails;
const styles = StyleSheet.create({
webBackContainer: {
alignSelf: "flex-start",
marginBottom: 12,
},
webBackButton: {
flexDirection: "row",
alignItems: "center",
gap: 8,
alignSelf: "flex-start",
paddingHorizontal: 14,
paddingVertical: 7,
borderRadius: 999,
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
backgroundColor: Palette.ultraLightWhite,
},
webBackIcon: {
width: 16,
height: 16,
transform: [{ rotate: "90deg" }],
},
webBackText: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
img: {
width: 200,
height: 200,
@@ -84,11 +84,6 @@ const SearchResultsList = ({
);
}
const fallbackThumbnail = thumbnailCandidates.find(isValid);
if (fallbackThumbnail) {
coverCandidates.push(fallbackThumbnail);
}
return coverCandidates.find(isValid) || null;
},
[]
+3 -1
View File
@@ -277,6 +277,7 @@ const PLAN_SEGMENTS = [
const HERO_IMAGE_WIDTH = isWeb ? 1280 : 520;
const HERO_IMAGE_HEIGHT = isWeb ? 760 : 360;
const PAGE_BACKGROUND_COLOR = "#303438";
export default function Payments() {
const route = useRoute();
@@ -498,6 +499,7 @@ export default function Payments() {
width={isWeb ? 960 : undefined}
containerStyle={styles.page}
contentContainerStyle={styles.pageContent}
backgroundColor={PAGE_BACKGROUND_COLOR}
>
<View style={styles.inner}>
<ExpoImage
@@ -597,7 +599,7 @@ export default function Payments() {
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: "#303438",
backgroundColor: PAGE_BACKGROUND_COLOR,
},
page: {
backgroundColor: "transparent",
+4 -4
View File
@@ -30,15 +30,15 @@ const Playback = ({ route }) => {
}}
>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<GradientButton
title="Enregistrer mon Playback"
onPress={onPressRecord}
/>
<BorderGradientButton
title="Guide du Playbacker"
onPress={() => setShowIntro(true)}
/>
{/* <BorderGradientButton title="Importer une vidéo" /> */}
<GradientButton
title="Enregistrer mon Playback"
onPress={onPressRecord}
/>
</View>
</View>
<FullscreenIntroVideo
+41 -3
View File
@@ -15,6 +15,7 @@ import GradientButton from "../../components/GradientButton";
import KaraokeLyrics from "../../components/KaraokeLyrics";
import MusicLandHeader from "../../components/MusicLandHeader";
import { increment, projectsRef } from "../../config/firebase";
import usePlayer from "../../hooks/usePlayer";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
@@ -35,6 +36,7 @@ const RecordPlayback = ({ route }) => {
log("Route params received", { hasProject: !!project });
const projectId = project?.id;
const songIndex = project?.songIndex;
const { setLooping, isLooping } = usePlayer() || {};
// Permissions
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
@@ -48,6 +50,8 @@ const RecordPlayback = ({ route }) => {
const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick
const playbackStartedRef = useRef(false); // devient vrai lorsque l'audio progresse réellement
const progressLogRef = useRef({ bucket: -1, lastPos: -1, lastDur: -1 });
const originalLoopingValueRef = useRef({ hasValue: false, value: false });
const latestLoopingValueRef = useRef(isLooping ?? false);
// Compteurs vues
const listenedMsRef = useRef(0);
@@ -80,6 +84,10 @@ const RecordPlayback = ({ route }) => {
log("Screen params", { projectId, songUrl, songIndex });
}, [projectId, songUrl, songIndex]);
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false;
}, [isLooping]);
const musicIndex = useMemo(() => {
const i = Number(songIndex);
return Number.isFinite(i) && i >= 0 ? i : 0;
@@ -280,12 +288,42 @@ const RecordPlayback = ({ route }) => {
} catch (_) {}
}, [player]);
const resetSessionRef = useRef(resetSession);
useEffect(() => {
resetSessionRef.current = resetSession;
}, [resetSession]);
useFocusEffect(
useCallback(() => {
log("Screen focused, resetting session");
void resetSession();
return () => {};
}, [resetSession])
void resetSessionRef.current?.();
return () => {
log("Screen blurred, stopping playback");
void resetSessionRef.current?.();
};
}, [])
);
useFocusEffect(
useCallback(() => {
if (typeof setLooping === "function") {
originalLoopingValueRef.current = {
hasValue: true,
value: latestLoopingValueRef.current,
};
if (latestLoopingValueRef.current) {
setLooping(false);
}
}
return () => {
if (
typeof setLooping === "function" &&
originalLoopingValueRef.current?.hasValue
) {
setLooping(!!originalLoopingValueRef.current.value);
}
};
}, [setLooping])
);
// Lancer le compte à rebours (le tick décrémente uniquement)
+41 -3
View File
@@ -16,6 +16,7 @@ import KaraokeLyrics from "../../components/KaraokeLyrics";
import MusicLandHeader from "../../components/MusicLandHeader";
import { increment, projectsRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType";
import usePlayer from "../../hooks/usePlayer";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
@@ -59,6 +60,7 @@ const RecordPlayback = ({ route }) => {
const { project } = route.params || {};
const songIndex = Number(project?.songIndex ?? 0) || 0;
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
const { setLooping, isLooping } = usePlayer() || {};
// Player
const songUrl = project?.songUrl || null;
@@ -80,6 +82,8 @@ const RecordPlayback = ({ route }) => {
const stopRecordingPromiseRef = useRef(null);
const stopRecordingResolveRef = useRef(null);
const recordedUrlRef = useRef(null);
const originalLoopingValueRef = useRef({ hasValue: false, value: false });
const latestLoopingValueRef = useRef(isLooping ?? false);
const [mediaReady, setMediaReady] = useState(false);
const [mediaError, setMediaError] = useState(null);
@@ -100,6 +104,10 @@ const RecordPlayback = ({ route }) => {
const correctedInitialJumpRef = useRef(false);
const progressDebugCounterRef = useRef(0);
useEffect(() => {
latestLoopingValueRef.current = isLooping ?? false;
}, [isLooping]);
// Lyrics
const alignedWords = useMemo(() => {
const ts = project?.musicTimestamps?.[songIndex];
@@ -328,11 +336,41 @@ const RecordPlayback = ({ route }) => {
console.log(LOG_PREFIX, "resetSession:end");
}, [player, releaseRecordingUrl, stopRecorderAndGetUrl]);
const resetSessionRef = useRef(resetSession);
useEffect(() => {
resetSessionRef.current = resetSession;
}, [resetSession]);
useFocusEffect(
useCallback(() => {
void resetSession();
return () => {};
}, [resetSession])
if (typeof setLooping === "function") {
originalLoopingValueRef.current = {
hasValue: true,
value: latestLoopingValueRef.current,
};
if (latestLoopingValueRef.current) {
setLooping(false);
}
}
return () => {
if (
typeof setLooping === "function" &&
originalLoopingValueRef.current?.hasValue
) {
setLooping(!!originalLoopingValueRef.current.value);
}
};
}, [setLooping])
);
useFocusEffect(
useCallback(() => {
void resetSessionRef.current?.();
return () => {
console.log(LOG_PREFIX, "focusEffect:cleanup");
void resetSessionRef.current?.();
};
}, [])
);
useEffect(() => {
+87 -12
View File
@@ -1,9 +1,9 @@
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import * as FileSystem from "expo-file-system";
import { VideoView, useVideoPlayer } from "expo-video";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import { background } from "../../assets";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Image, Pressable, View } from "react-native";
import { background, icons } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -30,8 +30,13 @@ const RecordedPlayback = ({ route }) => {
p.timeUpdateEventInterval = 0.2;
});
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const [progressInfo, setProgressInfo] = useState({
pos: 0,
dur: 0,
isPlaying: false,
});
const wasPlayingBeforeSeek = useRef(false);
const playbackEndedRef = useRef(false);
// Format mm:ss
const fmt = (ms) => {
@@ -44,7 +49,17 @@ const RecordedPlayback = ({ route }) => {
};
// Start both players on mount
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch (e) {}
try {
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
}, [audioPlayer, videoPlayer]);
useEffect(() => {
playbackEndedRef.current = false;
const start = async () => {
try {
if (audioPlayer && songUrl) await audioPlayer.play?.();
@@ -53,12 +68,10 @@ const RecordedPlayback = ({ route }) => {
};
start();
return () => {
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, videoPlayer, songUrl]);
}, [audioPlayer, songUrl, stopPlayback, videoPlayer]);
// Poll from audio player for progress display; keep video in sync if drifting
useEffect(() => {
@@ -66,7 +79,8 @@ const RecordedPlayback = ({ route }) => {
try {
const dur = (audioPlayer?.duration || 0) * 1000;
const pos = (audioPlayer?.currentTime || 0) * 1000;
setProgressInfo({ pos, dur });
const playing = !!audioPlayer?.playing || !!videoPlayer?.playing;
setProgressInfo({ pos, dur, isPlaying: playing });
// basic drift correction: if desync > 300ms, align video
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
@@ -94,6 +108,7 @@ const RecordedPlayback = ({ route }) => {
const onSeekStart = async () => {
try {
wasPlayingBeforeSeek.current = !!audioPlayer?.playing;
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
@@ -113,6 +128,47 @@ const RecordedPlayback = ({ route }) => {
: 0;
}, [progressInfo]);
useEffect(() => {
const duration = progressInfo?.dur || 0;
if (!duration) return;
const position = progressInfo?.pos || 0;
if (playbackEndedRef.current && duration - position > 1000) {
playbackEndedRef.current = false;
return;
}
const remaining = Math.max(0, duration - position);
if (remaining <= 400 && !playbackEndedRef.current) {
playbackEndedRef.current = true;
void stopPlayback();
}
}, [progressInfo, stopPlayback]);
const handleTogglePlayback = async () => {
try {
const duration = progressInfo?.dur || 0;
const position = progressInfo?.pos || 0;
const isAtEnd = duration > 0 && duration - position < 350;
const isCurrentlyPlaying =
!!audioPlayer?.playing || !!videoPlayer?.playing;
if (isCurrentlyPlaying) {
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
return;
}
if (isAtEnd) {
if (audioPlayer) await audioPlayer.seekTo?.(0);
if (videoPlayer) videoPlayer.currentTime = 0;
}
playbackEndedRef.current = false;
if (songUrl && audioPlayer) await audioPlayer.play?.();
if (videoPlayer) videoPlayer.play();
} catch (e) {}
};
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
@@ -144,6 +200,26 @@ const RecordedPlayback = ({ route }) => {
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
/>
<Pressable
onPress={handleTogglePlayback}
style={{
width: 72,
height: 72,
borderRadius: 36,
alignSelf: "center",
marginTop: 4,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(10, 5, 24, 0.65)",
borderWidth: 1,
borderColor: "#F94697",
}}
>
<Image
source={progressInfo.isPlaying ? icons.pause : icons.play}
style={{ width: 26, height: 26 }}
/>
</Pressable>
</View>
<View
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
@@ -162,8 +238,7 @@ const RecordedPlayback = ({ route }) => {
title="Recommencer"
onPress={async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
await stopPlayback();
} catch (e) {}
try {
if (videoUri) {
+35 -18
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Text, View } from "react-native";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
@@ -43,6 +43,7 @@ const RecordedPlayback = ({ route }) => {
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
const videoElRef = useRef(null);
const playbackEndedRef = useRef(false);
const [progress, setProgress] = useState({
posS: 0, // secondes
@@ -50,9 +51,20 @@ const RecordedPlayback = ({ route }) => {
playing: false,
});
const stopPlayback = useCallback(async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
}
} catch {}
}, [audioPlayer]);
// Démarrage / arrêt
useEffect(() => {
let mounted = true;
playbackEndedRef.current = false;
const start = async () => {
try {
if (audioPlayer && songUrl) {
@@ -68,17 +80,10 @@ const RecordedPlayback = ({ route }) => {
start();
return () => {
mounted = false;
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current) {
videoElRef.current.pause();
}
} catch {}
playbackEndedRef.current = false;
void stopPlayback();
};
}, [audioPlayer, songUrl, videoUri]);
}, [audioPlayer, songUrl, stopPlayback, videoUri]);
// Boucle de progression + éventuelle sync de la vidéo si fournie
useEffect(() => {
@@ -115,6 +120,21 @@ const RecordedPlayback = ({ route }) => {
return progress.durS > 0 ? Math.min(1, progress.posS / progress.durS) : 0;
}, [progress]);
useEffect(() => {
const duration = progress?.durS || 0;
if (!duration) return;
const position = progress?.posS || 0;
if (playbackEndedRef.current && duration - position > 1) {
playbackEndedRef.current = false;
return;
}
const remaining = Math.max(0, duration - position);
if (remaining <= 0.35 && !playbackEndedRef.current) {
playbackEndedRef.current = true;
void stopPlayback();
}
}, [progress, stopPlayback]);
const onSeek = async (ratio) => {
try {
const durS = Number(progress.durS || 0);
@@ -132,6 +152,7 @@ const RecordedPlayback = ({ route }) => {
const onSeekStart = async () => {
try {
wasPlayingRef.current = !!audioPlayer?.playing;
playbackEndedRef.current = false;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
@@ -244,13 +265,9 @@ const RecordedPlayback = ({ route }) => {
/>
<BorderGradientButton
title="Recommencer"
onPress={() => {
onPress={async () => {
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current && !videoElRef.current.paused)
videoElRef.current.pause();
await stopPlayback();
} catch {}
navigate(Routes.RecordPlayback, { project });
}}
+12 -1
View File
@@ -3,6 +3,7 @@ import React, { useCallback, useMemo, useState } from "react";
import { Alert, Platform, StyleSheet, Text, View } from "react-native";
import BorderGradientButton from "../../components/BorderGradientButton";
import { background } from "../../assets";
import { getFunctionsClient } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation/Routes";
@@ -41,6 +42,8 @@ const PERIOD_LABELS = {
annual: "Annuel",
};
const PAGE_BACKGROUND_COLOR = "#303438";
const formatCoinsAmount = (value) => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return null;
@@ -560,8 +563,16 @@ const ManageSubscription = ({ navigation }) => {
? Palette.grayMid
: Palette.red;
const backgroundImage = background.bgTrans;
return (
<Page headerType="NAVIGATE" title="Mon abonnement" scrollEnabled>
<Page
headerType="NAVIGATE"
title="Mon abonnement"
scrollEnabled
backgroundColor={PAGE_BACKGROUND_COLOR}
backgroundImg={backgroundImage}
>
<View style={styles.container}>
{subscriptionInfo.hasAnySubscription ? (
<>
+14 -5
View File
@@ -7,13 +7,22 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import { RHYTHM } from "../../data/data";
import ListSelection from "../../components/ListSelection/ListSelection";
const ChooseRhythm = ({ selected, setSelected }) => {
const ITEM_HEIGHT = 54;
const CONTENT_GAP = 10;
const CONTENT_PADDING = 8;
const EXTRA_HEADROOM = 80; // extra space keeps the list fully visible without scrolling
const RHYTHM_CONTAINER_HEIGHT =
RHYTHM.length * ITEM_HEIGHT +
Math.max(RHYTHM.length - 1, 0) * CONTENT_GAP +
CONTENT_PADDING * 2 +
EXTRA_HEADROOM;
const ChooseRhythm = ({ selected, setSelected }) => {
return (
<View style={{ flex: 1, gap: 10, paddingTop: 16 }}>
<CreateLyricsHeader title="Choisis un rythme" />
<View style={{ flex: 1 }}>
<ItemContainer height={340}>
<ItemContainer height={RHYTHM_CONTAINER_HEIGHT}>
<ListSelection
options={RHYTHM}
variant="simple"
@@ -34,12 +43,12 @@ export default ChooseRhythm;
const styles = StyleSheet.create({
contentContainer: {
flexGrow: 1,
padding: 8,
gap: 10,
padding: CONTENT_PADDING,
gap: CONTENT_GAP,
backgroundColor: "#FFFFFF00",
},
itemContainer: {
height: 54,
height: ITEM_HEIGHT,
backgroundColor: Palette.glass,
borderRadius: 14,
overflow: "hidden",
-22
View File
@@ -264,32 +264,10 @@ const GeneratingSong = () => {
/>
<View
style={{
height: responsiveHeight(50),
marginTop: responsiveHeight(10),
gap: 16,
}}
>
<View
style={{
zIndex: 1,
position: "absolute",
top: -150,
width: "50%",
height: "100%",
alignSelf: "center",
}}
>
<Image
source={ai.malik}
style={{
width: "100%",
height: isWeb ? 300 : "60%",
right: -10,
top: 50,
}}
resizeMode="contain"
/>
</View>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
+6 -8
View File
@@ -36,8 +36,6 @@ const SongReady = () => {
1: { pos: 0, dur: 0 },
});
console.log("progress info", JSON.stringify(progressInfo, null, 2));
const player0 = useSharedAudioPlayer(
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
{
@@ -51,7 +49,7 @@ const SongReady = () => {
artwork: selectedProject?.coverUrl || null,
coverUrl: selectedProject?.coverUrl || null,
metadata: { index: 0, projectId },
}
},
);
const player1 = useSharedAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
@@ -66,7 +64,7 @@ const SongReady = () => {
artwork: selectedProject?.coverUrl || null,
coverUrl: selectedProject?.coverUrl || null,
metadata: { index: 1, projectId },
}
},
);
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
const prefetchedRef = useRef({
@@ -288,7 +286,7 @@ const SongReady = () => {
player1?.pause?.();
} catch {}
};
}, [player0, player1])
}, [player0, player1]),
);
useEffect(() => {
@@ -318,7 +316,7 @@ const SongReady = () => {
},
},
],
{ cancelable: false }
{ cancelable: false },
);
return;
}
@@ -425,7 +423,7 @@ const SongReady = () => {
} catch (e) {
console.log(
"SongReady pause on seek start",
e?.message
e?.message,
);
}
}}
@@ -441,7 +439,7 @@ const SongReady = () => {
} catch (e) {
console.log(
"SongReady resume after seek",
e?.message
e?.message,
);
}
}}
+3 -2
View File
@@ -9,6 +9,7 @@ import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { isWeb } from "../../hooks/useLayoutType";
const Studio = () => {
const [selectedProjectId, setSelectedProjectId] = useState(null);
@@ -23,7 +24,7 @@ const Studio = () => {
return (
<Page
headerType="NONE"
backgroundImg={background.studioBG}
backgroundImg={isWeb ? background.productionBG2 : background.studioBG}
containerStyle={{ paddingHorizontal: 0 }}
contentContainerStyle={{
padding: gutters * 2,
@@ -117,7 +118,7 @@ const Studio = () => {
if (selectedProject?.musicStatus === "GENERATING") {
alert(
"Attention",
"La chanson est en cours de génération. Veuillez patienter."
"La chanson est en cours de génération. Veuillez patienter.",
);
} else {
if (!selectedProject?.id) return;
+18 -13
View File
@@ -16,6 +16,7 @@ import { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { buildFullName } from "../../utils/artistName";
import { background } from "../../assets";
export const ChooseCoverType = () => {
const [showIntro, setShowIntro] = useState(true);
@@ -26,14 +27,9 @@ export const ChooseCoverType = () => {
const [savingChoice, setSavingChoice] = useState(false);
const [savingPseudo, setSavingPseudo] = useState(false);
const {
selectedProject,
selectedProjectId,
updateProjectData,
currentUserData,
currentUID,
} = useUserData();
const { setIsLoading, setTooltip } = useMinuit();
const { selectedProject, selectedProjectId, currentUserData, currentUID } =
useUserData();
const { setTooltip } = useMinuit();
const projectId = selectedProject?.id || selectedProjectId || null;
const hasFinalCover = !!selectedProject?.coverUrl;
@@ -53,7 +49,7 @@ export const ChooseCoverType = () => {
currentUserData?.firstName,
currentUserData?.lastName,
currentUserData?.displayName,
]
],
);
useEffect(() => {
@@ -73,7 +69,7 @@ export const ChooseCoverType = () => {
setPendingAction(() => nextAction);
setChoiceVisible(true);
},
[hasArtistPreference]
[hasArtistPreference],
);
const runPendingAction = useCallback(async () => {
@@ -117,7 +113,7 @@ export const ChooseCoverType = () => {
console.log("update current project userName error", error?.message);
}
},
[currentUID, projectId]
[currentUID, projectId],
);
// const pickUserImage = useCallback(async () => {
@@ -252,7 +248,7 @@ export const ChooseCoverType = () => {
artistNamePreference: "CUSTOM",
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
{ merge: true },
);
await applyDisplayNameToProjects(value);
setTooltip({ type: "success", text: "Pseudo enregistré" });
@@ -277,7 +273,7 @@ export const ChooseCoverType = () => {
return (
<Page
// backgroundImg={background.studioBG2}
backgroundImg={background.studioBG2}
backgroundColor={"#2A2E33"}
headerType="NONE"
>
@@ -373,6 +369,9 @@ export const ChooseCoverType = () => {
<Text style={styles.modalDescription}>
Choisis le nom qui sera visible sur tes musiques.
</Text>
<Text style={styles.modalWarning}>
Attention : tu ne pourras plus le modifier ensuite.
</Text>
<View style={styles.inputWrapper}>
<Input
placeholder="Nom d'artiste"
@@ -437,6 +436,12 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
},
modalWarning: {
fontSize: 13,
color: Palette.orange,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
},
modalButtons: {
width: "80%",
alignSelf: "center",
+64 -17
View File
@@ -10,8 +10,7 @@ import {
TextInput,
View,
} from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { icons } from "../../assets";
import { background, icons } from "../../assets";
import alert from "../../components/Alert";
import AppCheckbox from "../../components/AppCheckbox";
import BorderGradientButton from "../../components/BorderGradientButton";
@@ -20,6 +19,7 @@ import MusicLandHeader from "../../components/MusicLandHeader";
import firebase, { tasksRef } from "../../config/firebase";
import loaderMessages from "../../config/loaderMessages";
import { isWeb } from "../../hooks/useLayoutType";
import useGlobalLoading from "../../hooks/useGlobalLoading";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
@@ -38,7 +38,7 @@ const COVER_STYLE_PRESETS = [
const PouchReady = () => {
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
const { setIsLoading } = useMinuit();
const { setLoading } = useGlobalLoading();
const isGenerating = selectedProject?.coverStatus === "GENERATING";
const coverOptions = useMemo(() => {
if (!Array.isArray(selectedProject?.cover?.options)) {
@@ -53,21 +53,37 @@ const PouchReady = () => {
return null;
}
const found = coverOptions.find(
(option) => option?.id === selectedOptionId
(option) => option?.id === selectedOptionId,
);
return found || coverOptions[0] || null;
}, [coverOptions, selectedOptionId]);
const coverBackgroundMessage = isWeb
? loaderMessages.pouchReadyGenerationWeb
: "";
const generationLoadingMessage = useMemo(() => {
if (typeof coverBackgroundMessage === "string") {
const trimmedMessage = coverBackgroundMessage.trim();
if (trimmedMessage.length) {
return trimmedMessage;
}
}
return "Nous lançons la génération de ta pochette...";
}, [coverBackgroundMessage]);
const [styleMode, setStyleMode] = useState("preset");
const [selectedPresetStyle, setSelectedPresetStyle] = useState(
COVER_STYLE_PRESETS[0]
COVER_STYLE_PRESETS[0],
);
const [customStyle, setCustomStyle] = useState("");
const [isPresetDropdownOpen, setIsPresetDropdownOpen] = useState(false);
const [isSelecting, setIsSelecting] = useState(false);
const [isAwaitingGenerationStart, setIsAwaitingGenerationStart] =
useState(false);
const showGenerationLoading = useCallback(async () => {
setIsAwaitingGenerationStart(true);
await setLoading(true, { message: generationLoadingMessage });
}, [generationLoadingMessage, setLoading]);
useEffect(() => {
const projectStyle = (selectedProject?.coverStyle || "").trim();
@@ -106,12 +122,12 @@ const PouchReady = () => {
return;
}
try {
await setIsLoading(true);
await showGenerationLoading();
await updateProjectData(
{
coverStyle: trimmedStyle,
},
{ merge: true }
{ merge: true },
);
await tasksRef.add({
type: "cover",
@@ -121,11 +137,17 @@ const PouchReady = () => {
});
} catch (e) {
console.log("Cover task error", e?.message);
} finally {
setIsLoading(false);
setIsAwaitingGenerationStart(false);
await setLoading(false);
}
},
[hasGeneratedOptions, selectedProjectId, setIsLoading, updateProjectData]
[
hasGeneratedOptions,
selectedProjectId,
setLoading,
showGenerationLoading,
updateProjectData,
],
);
const requestCoverGeneration = useCallback(() => {
@@ -196,7 +218,7 @@ const PouchReady = () => {
selectedOptionId,
selectedProject?.cover,
updateProjectData,
]
],
);
const onValidatePicture = useCallback(async () => {
@@ -204,7 +226,9 @@ const PouchReady = () => {
return;
}
try {
await setIsLoading(true);
await setLoading(true, {
message: "Validation de la pochette en cours...",
});
const existingCover = selectedProject?.cover || {};
const finalUrl =
selectedOption.finalUrl || selectedOption.generatedUrl || null;
@@ -226,7 +250,7 @@ const PouchReady = () => {
coverUrl: finalUrl,
};
const playbackStage = getStageAction("director", projectForStage);
await setIsLoading(false);
await setLoading(false);
alert(
"Malik",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec John pour produire ton playback.",
@@ -246,19 +270,19 @@ const PouchReady = () => {
navigate(targetRoute, params);
},
},
]
],
);
} catch (e) {
console.log("PouchReady: unable to validate cover", e?.message);
} finally {
await setIsLoading(false);
await setLoading(false);
}
}, [
coverOptions,
navigate,
selectedOption,
selectedProject,
setIsLoading,
setLoading,
updateProjectData,
]);
@@ -272,8 +296,31 @@ const PouchReady = () => {
isGenerating ||
(hasGeneratedOptions ? !selectedOption || isSelecting : true);
useEffect(() => {
if (!isAwaitingGenerationStart) {
return;
}
if (isGenerating) {
setIsAwaitingGenerationStart(false);
setLoading(false);
}
}, [isAwaitingGenerationStart, isGenerating, setLoading]);
useEffect(
() => () => {
if (isAwaitingGenerationStart) {
setIsAwaitingGenerationStart(false);
setLoading(false);
}
},
[isAwaitingGenerationStart, setLoading],
);
return (
<Page backgroundColor={"#2A2E33"} headerType="NONE">
<Page
backgroundImg={isWeb ? background.productionBG2 : background.studioBG}
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={72} />
<View style={{ flex: 1, marginTop: 0 }}>
<CreateLyricsHeader