clear lot of tickets
This commit is contained in:
@@ -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 } : {}),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}}
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user