fix: blur and music duration
This commit is contained in:
@@ -58,7 +58,7 @@ async function buildCoverWithLogo(backgroundUrl, targetPath) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||||
targetPath,
|
targetPath
|
||||||
)}?alt=media&token=${token}`;
|
)}?alt=media&token=${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ async function performCoverGeneration(project) {
|
|||||||
coverStatus: "GENERATED",
|
coverStatus: "GENERATED",
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
},
|
},
|
||||||
{ merge: true },
|
{ merge: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info("✅ [Cover] Saved", {
|
logger.info("✅ [Cover] Saved", {
|
||||||
@@ -280,7 +280,7 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
|||||||
});
|
});
|
||||||
await event.data.ref.set(
|
await event.data.ref.set(
|
||||||
{ status: "ERROR", error: error?.message || "Erreur" },
|
{ status: "ERROR", error: error?.message || "Erreur" },
|
||||||
{ merge: true },
|
{ merge: true }
|
||||||
);
|
);
|
||||||
await refList.projects.doc(projectId).update({
|
await refList.projects.doc(projectId).update({
|
||||||
coverStatus: "ERROR",
|
coverStatus: "ERROR",
|
||||||
@@ -318,5 +318,5 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { View, Text, ScrollView, StyleSheet, Dimensions } from "react-native";
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Dimensions,
|
||||||
|
TouchableOpacity,
|
||||||
|
} from "react-native";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
@@ -50,7 +57,6 @@ const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
|
|||||||
[sourceItems],
|
[sourceItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [leftItems, setLeftItems] = useState(normalizedSource);
|
|
||||||
const [rightItems, setRightItems] = useState([]);
|
const [rightItems, setRightItems] = useState([]);
|
||||||
|
|
||||||
// Compare arrays by item id and order to avoid unnecessary state churn
|
// Compare arrays by item id and order to avoid unnecessary state churn
|
||||||
@@ -67,13 +73,16 @@ const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selected = Array.isArray(initialSelected) ? initialSelected : [];
|
const selected = Array.isArray(initialSelected) ? initialSelected : [];
|
||||||
const selectedSet = new Set(selected);
|
const selectedSet = new Set(selected);
|
||||||
const right = normalizedSource.filter((i) => selectedSet.has(i.id));
|
const nextRight = normalizedSource.filter((i) => selectedSet.has(i.id));
|
||||||
const left = normalizedSource.filter((i) => !selectedSet.has(i.id));
|
setRightItems((prev) => (sameById(prev, nextRight) ? prev : nextRight));
|
||||||
|
|
||||||
setLeftItems((prev) => (sameById(prev, left) ? prev : left));
|
|
||||||
setRightItems((prev) => (sameById(prev, right) ? prev : right));
|
|
||||||
}, [normalizedSource, initialSelected]);
|
}, [normalizedSource, initialSelected]);
|
||||||
|
|
||||||
|
const leftItems = useMemo(() => {
|
||||||
|
if (!rightItems || rightItems.length === 0) return normalizedSource;
|
||||||
|
const rightIds = new Set(rightItems.map((item) => item.id));
|
||||||
|
return normalizedSource.filter((item) => !rightIds.has(item.id));
|
||||||
|
}, [normalizedSource, rightItems]);
|
||||||
|
|
||||||
const draggingItem = useSharedValue(null);
|
const draggingItem = useSharedValue(null);
|
||||||
const translateX = useSharedValue(0);
|
const translateX = useSharedValue(0);
|
||||||
const translateY = useSharedValue(0);
|
const translateY = useSharedValue(0);
|
||||||
@@ -83,15 +92,22 @@ const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const moveItem = (itemId) => {
|
const moveItem = (itemId) => {
|
||||||
setLeftItems((items) => {
|
const item = normalizedSource.find((i) => i.id === itemId);
|
||||||
const found = items.find((i) => i.id === itemId);
|
if (!item) return;
|
||||||
if (!found) return items;
|
setRightItems((current) => {
|
||||||
setRightItems((r) => {
|
if (current.some((i) => i.id === itemId)) return current;
|
||||||
const next = [...r, found];
|
const next = [...current, item];
|
||||||
onChange?.(next.map((i) => i.value ?? i.label));
|
onChange?.(next.map((i) => i.value ?? i.label));
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
return items.filter((i) => i.id !== itemId);
|
};
|
||||||
|
|
||||||
|
const removeItem = (itemId) => {
|
||||||
|
setRightItems((current) => {
|
||||||
|
if (!current.some((i) => i.id === itemId)) return current;
|
||||||
|
const next = current.filter((i) => i.id !== itemId);
|
||||||
|
onChange?.(next.map((i) => i.value ?? i.label));
|
||||||
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,10 +149,19 @@ const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
|
|||||||
|
|
||||||
<ScrollView style={{ ...styles.scroll, zIndex: -1 }}>
|
<ScrollView style={{ ...styles.scroll, zIndex: -1 }}>
|
||||||
<Text style={styles.title}>Right</Text>
|
<Text style={styles.title}>Right</Text>
|
||||||
|
<Text style={styles.helpText}>Appuyez pour retirer un élément</Text>
|
||||||
{rightItems.map((item) => (
|
{rightItems.map((item) => (
|
||||||
<View key={item.id} style={[styles.item, { backgroundColor: "#bdf" }]}>
|
<TouchableOpacity
|
||||||
|
key={item.id}
|
||||||
|
style={styles.selectedItem}
|
||||||
|
onPress={() => removeItem(item.id)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Retirer ${item.label ?? String(item)}`}
|
||||||
|
>
|
||||||
<Text style={styles.text}>{item.label ?? String(item)}</Text>
|
<Text style={styles.text}>{item.label ?? String(item)}</Text>
|
||||||
</View>
|
<Text style={styles.removeHint}>Retirer</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
@@ -169,6 +194,24 @@ const styles = StyleSheet.create({
|
|||||||
marginVertical: 5,
|
marginVertical: 5,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
},
|
},
|
||||||
|
selectedItem: {
|
||||||
|
backgroundColor: "#bdf",
|
||||||
|
padding: 15,
|
||||||
|
marginVertical: 5,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
helpText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#666",
|
||||||
|
marginBottom: 8,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
removeHint: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#555",
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
color: "#333",
|
color: "#333",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const useSharedAudioPlayer = (source, options = null) => {
|
|||||||
pause,
|
pause,
|
||||||
resume,
|
resume,
|
||||||
seekTo,
|
seekTo,
|
||||||
|
ensureLoaded,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
positionMs,
|
positionMs,
|
||||||
durationMs,
|
durationMs,
|
||||||
@@ -84,10 +85,16 @@ const useSharedAudioPlayer = (source, options = null) => {
|
|||||||
};
|
};
|
||||||
}, [isPlaying, positionMs, durationMs]);
|
}, [isPlaying, positionMs, durationMs]);
|
||||||
|
|
||||||
const controlsRef = useRef({ play, pause, resume, seekTo });
|
const controlsRef = useRef({
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
resume,
|
||||||
|
seekTo,
|
||||||
|
ensureLoaded,
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
controlsRef.current = { play, pause, resume, seekTo };
|
controlsRef.current = { play, pause, resume, seekTo, ensureLoaded };
|
||||||
}, [play, pause, resume, seekTo]);
|
}, [play, pause, resume, seekTo, ensureLoaded]);
|
||||||
|
|
||||||
const playerRef = useRef(null);
|
const playerRef = useRef(null);
|
||||||
|
|
||||||
@@ -138,6 +145,15 @@ const useSharedAudioPlayer = (source, options = null) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
player.load = async ({ startPositionMs = 0 } = {}) => {
|
||||||
|
if (typeof controlsRef.current.ensureLoaded !== "function") return;
|
||||||
|
const target = Math.max(0, Number(startPositionMs) || 0);
|
||||||
|
await controlsRef.current.ensureLoaded({
|
||||||
|
startPositionMs: target,
|
||||||
|
autoPlay: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
playerRef.current = player;
|
playerRef.current = player;
|
||||||
}
|
}
|
||||||
}, [descriptor]);
|
}, [descriptor]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import usePlayer from "./usePlayer";
|
import usePlayer from "./usePlayer";
|
||||||
|
|
||||||
const useTrackController = (descriptor) => {
|
const useTrackController = (descriptor) => {
|
||||||
@@ -19,10 +19,38 @@ const useTrackController = (descriptor) => {
|
|||||||
return descriptor;
|
return descriptor;
|
||||||
}, [descriptor]);
|
}, [descriptor]);
|
||||||
|
|
||||||
|
const lastKnownRef = useRef({
|
||||||
|
descriptorId: null,
|
||||||
|
positionMs: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const trackId = normalizedDescriptor?.id || null;
|
const trackId = normalizedDescriptor?.id || null;
|
||||||
const isCurrent = trackId ? currentTrack?.id === trackId : false;
|
const isCurrent = trackId ? currentTrack?.id === trackId : false;
|
||||||
const effectivePositionMs = isCurrent ? positionMs : 0;
|
const descriptorId = normalizedDescriptor?.id ?? null;
|
||||||
const effectiveDurationMs = isCurrent ? durationMs : 0;
|
|
||||||
|
if (lastKnownRef.current.descriptorId !== descriptorId) {
|
||||||
|
lastKnownRef.current = {
|
||||||
|
descriptorId,
|
||||||
|
positionMs: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
lastKnownRef.current = {
|
||||||
|
descriptorId,
|
||||||
|
positionMs,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectivePositionMs = isCurrent
|
||||||
|
? positionMs
|
||||||
|
: lastKnownRef.current.positionMs;
|
||||||
|
const effectiveDurationMs = isCurrent
|
||||||
|
? durationMs
|
||||||
|
: lastKnownRef.current.durationMs;
|
||||||
const effectiveIsPlaying = isCurrent && isPlaying;
|
const effectiveIsPlaying = isCurrent && isPlaying;
|
||||||
|
|
||||||
const ensureLoaded = useCallback(
|
const ensureLoaded = useCallback(
|
||||||
@@ -140,4 +168,3 @@ const useTrackController = (descriptor) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default useTrackController;
|
export default useTrackController;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
import { Image, Pressable, Text, View } from "react-native";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { background, icons } from "../../assets";
|
import { background, icons } from "../../assets";
|
||||||
import alert from "../../components/Alert";
|
import alert from "../../components/Alert";
|
||||||
@@ -35,6 +35,9 @@ const SongReady = () => {
|
|||||||
0: { pos: 0, dur: 0 },
|
0: { pos: 0, dur: 0 },
|
||||||
1: { pos: 0, dur: 0 },
|
1: { pos: 0, dur: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("progress info", JSON.stringify(progressInfo, null, 2));
|
||||||
|
|
||||||
const player0 = useSharedAudioPlayer(
|
const player0 = useSharedAudioPlayer(
|
||||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
||||||
{
|
{
|
||||||
@@ -66,6 +69,10 @@ const SongReady = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
|
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
|
||||||
|
const prefetchedRef = useRef({
|
||||||
|
0: { url: null, done: false },
|
||||||
|
1: { url: null, done: false },
|
||||||
|
});
|
||||||
|
|
||||||
// Sync URLs from provider's selectedProject
|
// Sync URLs from provider's selectedProject
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -75,6 +82,100 @@ const SongReady = () => {
|
|||||||
setMusicUrls(urls);
|
setMusicUrls(urls);
|
||||||
}, [selectedProject?.musicUrls]);
|
}, [selectedProject?.musicUrls]);
|
||||||
|
|
||||||
|
const firstUrl = musicUrls[0] ?? null;
|
||||||
|
const secondUrl = musicUrls[1] ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
|
const waitForDuration = async (index) => {
|
||||||
|
const getDurationSeconds = () => {
|
||||||
|
const targetPlayer = index === 0 ? player0 : player1;
|
||||||
|
if (!targetPlayer) return 0;
|
||||||
|
const raw = Number(targetPlayer.duration || 0);
|
||||||
|
return Number.isFinite(raw) ? raw : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
while (!isCancelled) {
|
||||||
|
const durationSeconds = getDurationSeconds();
|
||||||
|
if (durationSeconds > 0) {
|
||||||
|
const durationMs = Math.round(durationSeconds * 1000);
|
||||||
|
setProgressInfo((prev) => {
|
||||||
|
const current = prev?.[index] || { pos: 0, dur: 0 };
|
||||||
|
if (Math.abs(current.dur - durationMs) < 5) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[index]: {
|
||||||
|
...current,
|
||||||
|
dur: durationMs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Date.now() - start > 6000) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefetch = async () => {
|
||||||
|
const entries = [
|
||||||
|
[0, firstUrl, player0],
|
||||||
|
[1, secondUrl, player1],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [idx, url, player] of entries) {
|
||||||
|
const cacheEntry = prefetchedRef.current[idx];
|
||||||
|
|
||||||
|
if (!url || !player?.load) {
|
||||||
|
if (cacheEntry) {
|
||||||
|
cacheEntry.url = url ?? null;
|
||||||
|
cacheEntry.done = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheEntry && cacheEntry.url === url && cacheEntry.done) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheEntry) {
|
||||||
|
cacheEntry.url = url;
|
||||||
|
cacheEntry.done = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await player.load({ startPositionMs: 0 });
|
||||||
|
const resolved = await waitForDuration(idx);
|
||||||
|
if (cacheEntry && !isCancelled) {
|
||||||
|
cacheEntry.done = resolved;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (cacheEntry) {
|
||||||
|
cacheEntry.done = false;
|
||||||
|
}
|
||||||
|
if (!isCancelled) {
|
||||||
|
console.log("SongReady preload error", err?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
prefetch();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [firstUrl, secondUrl, player0, player1]);
|
||||||
|
|
||||||
// Sync progression depuis les players
|
// Sync progression depuis les players
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Ensure we consistently work with milliseconds whatever the platform
|
// Ensure we consistently work with milliseconds whatever the platform
|
||||||
@@ -264,7 +365,7 @@ const SongReady = () => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<BlurView
|
<BlurView
|
||||||
intensity={Platform.OS !== "ios" ? 10 : 40}
|
intensity={40}
|
||||||
tint="dark"
|
tint="dark"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { View, Text, ScrollView, Pressable } from "react-native";
|
import { View, Text, ScrollView, Pressable } from "react-native";
|
||||||
import React, { useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { background } from "../../assets";
|
import { background } from "../../assets";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
@@ -11,10 +11,15 @@ import { useUser } from "../../providers/UserDataProvider";
|
|||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
|
|
||||||
const Studio = () => {
|
const Studio = () => {
|
||||||
const [selected, setSelected] = useState(null);
|
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
||||||
|
|
||||||
const { userProjects: projects, selectProject } = useUser();
|
const { userProjects: projects, selectProject } = useUser();
|
||||||
|
|
||||||
|
const selectedProject = useMemo(() => {
|
||||||
|
if (!Array.isArray(projects) || !projects.length) return null;
|
||||||
|
return projects.find((p) => p?.id === selectedProjectId) ?? null;
|
||||||
|
}, [projects, selectedProjectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
headerType="NONE"
|
headerType="NONE"
|
||||||
@@ -41,7 +46,7 @@ const Studio = () => {
|
|||||||
{projects.map((p) => {
|
{projects.map((p) => {
|
||||||
const couplet = p?.lyrics?.couplet || "";
|
const couplet = p?.lyrics?.couplet || "";
|
||||||
const preview = couplet.split("\n").slice(0, 2).join(" ");
|
const preview = couplet.split("\n").slice(0, 2).join(" ");
|
||||||
const isSelected = selected === p;
|
const isSelected = selectedProjectId === p?.id;
|
||||||
const createdAt = p?.createdAt?.toDate
|
const createdAt = p?.createdAt?.toDate
|
||||||
? p.createdAt.toDate()
|
? p.createdAt.toDate()
|
||||||
: p?.createdAt
|
: p?.createdAt
|
||||||
@@ -54,13 +59,20 @@ const Studio = () => {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onPress={() => setSelected(p)}
|
onPress={() => setSelectedProjectId(p?.id ?? null)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#0F0C1933",
|
backgroundColor: isSelected
|
||||||
|
? "rgba(15, 12, 25, 0.75)"
|
||||||
|
: "rgba(15, 12, 25, 0.2)",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderWidth: isSelected ? 2 : 0,
|
borderWidth: isSelected ? 2 : 0,
|
||||||
borderColor: isSelected ? "#F94697" : "transparent",
|
borderColor: isSelected ? "#F94697" : "transparent",
|
||||||
|
shadowColor: "rgba(0, 0, 0, 0.55)",
|
||||||
|
shadowOpacity: isSelected ? 0.6 : 0.35,
|
||||||
|
shadowRadius: isSelected ? 8 : 4,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
elevation: isSelected ? 6 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@@ -100,20 +112,21 @@ const Studio = () => {
|
|||||||
<View style={{ justifyContent: "flex-end" }}>
|
<View style={{ justifyContent: "flex-end" }}>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Commencer"
|
title="Commencer"
|
||||||
disabled={!selected}
|
disabled={!selectedProject}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (selected.musicStatus === "GENERATING") {
|
if (selectedProject?.musicStatus === "GENERATING") {
|
||||||
alert(
|
alert(
|
||||||
"Attention",
|
"Attention",
|
||||||
"La chanson est en cours de génération. Veuillez patienter.",
|
"La chanson est en cours de génération. Veuillez patienter.",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
selectProject(selected.id);
|
if (!selectedProject?.id) return;
|
||||||
|
selectProject(selectedProject.id);
|
||||||
navigate(Routes.Compose);
|
navigate(Routes.Compose);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{selected?.musicUrls?.length > 0 && (
|
{selectedProject?.musicUrls?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Ecouter les audios"
|
title="Ecouter les audios"
|
||||||
@@ -121,18 +134,20 @@ const Studio = () => {
|
|||||||
marginTop: responsiveHeight(2),
|
marginTop: responsiveHeight(2),
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
selectProject(selected.id);
|
if (!selectedProject?.id) return;
|
||||||
|
selectProject(selectedProject.id);
|
||||||
navigate(Routes.SongReady);
|
navigate(Routes.SongReady);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!selected?.coverUrl && (
|
{!selectedProject?.coverUrl && (
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Générer une pochette"
|
title="Générer une pochette"
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
marginTop: responsiveHeight(2),
|
marginTop: responsiveHeight(2),
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
selectProject(selected.id);
|
if (!selectedProject?.id) return;
|
||||||
|
selectProject(selectedProject.id);
|
||||||
navigate(Routes.PouchReady);
|
navigate(Routes.PouchReady);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -188,6 +188,36 @@ const CreateLyricsWithAi = () => {
|
|||||||
}
|
}
|
||||||
}, [structure]);
|
}, [structure]);
|
||||||
|
|
||||||
|
const sanitizedCustomStructure = useMemo(() => {
|
||||||
|
if (!Array.isArray(customStructure) || customStructure.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS);
|
||||||
|
}, [customStructure]);
|
||||||
|
|
||||||
|
const sanitizedParsedStructure = useMemo(() => {
|
||||||
|
if (!Array.isArray(parsedStructure) || parsedStructure.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return sanitizeStructureList(parsedStructure, CUSTOM_SANITIZE_OPTIONS);
|
||||||
|
}, [parsedStructure]);
|
||||||
|
|
||||||
|
const fallbackProjectStructure = useMemo(() => {
|
||||||
|
const raw = selectedProject?.config?.structure;
|
||||||
|
if (!Array.isArray(raw) || raw.length === 0) return [];
|
||||||
|
return sanitizeStructureList(raw, CUSTOM_SANITIZE_OPTIONS);
|
||||||
|
}, [selectedProject]);
|
||||||
|
|
||||||
|
const baseStructureForCustomizer = useMemo(() => {
|
||||||
|
if (sanitizedCustomStructure.length > 0) return sanitizedCustomStructure;
|
||||||
|
if (sanitizedParsedStructure.length > 0) return sanitizedParsedStructure;
|
||||||
|
return fallbackProjectStructure;
|
||||||
|
}, [
|
||||||
|
sanitizedCustomStructure,
|
||||||
|
sanitizedParsedStructure,
|
||||||
|
fallbackProjectStructure,
|
||||||
|
]);
|
||||||
|
|
||||||
const lyricsConfig = useMemo(() => {
|
const lyricsConfig = useMemo(() => {
|
||||||
const resolvedObjective = shouldUseOtherObjective
|
const resolvedObjective = shouldUseOtherObjective
|
||||||
? persistedOtherObjective || undefined
|
? persistedOtherObjective || undefined
|
||||||
@@ -195,13 +225,8 @@ const CreateLyricsWithAi = () => {
|
|||||||
const resolvedStyle = shouldUseOtherStyle
|
const resolvedStyle = shouldUseOtherStyle
|
||||||
? persistedOtherStyle || undefined
|
? persistedOtherStyle || undefined
|
||||||
: style || undefined;
|
: style || undefined;
|
||||||
const sanitizedCustom =
|
const sanitizedCustom = sanitizedCustomStructure;
|
||||||
Array.isArray(customStructure) && customStructure.length > 0
|
const sanitizedParsed = sanitizedParsedStructure;
|
||||||
? sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS)
|
|
||||||
: [];
|
|
||||||
const sanitizedParsed = Array.isArray(parsedStructure)
|
|
||||||
? sanitizeStructureList(parsedStructure)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objective: resolvedObjective,
|
objective: resolvedObjective,
|
||||||
@@ -230,9 +255,9 @@ const CreateLyricsWithAi = () => {
|
|||||||
emotion,
|
emotion,
|
||||||
style,
|
style,
|
||||||
audience,
|
audience,
|
||||||
parsedStructure,
|
sanitizedParsedStructure,
|
||||||
rhymes,
|
rhymes,
|
||||||
customStructure,
|
sanitizedCustomStructure,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isNextDisabled = React.useMemo(() => {
|
const isNextDisabled = React.useMemo(() => {
|
||||||
@@ -444,14 +469,7 @@ const CreateLyricsWithAi = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CustomizeSongStructure
|
<CustomizeSongStructure
|
||||||
baseStructure={
|
baseStructure={baseStructureForCustomizer}
|
||||||
parsedStructure ||
|
|
||||||
(Array.isArray(customStructure)
|
|
||||||
? customStructure
|
|
||||||
: Array.isArray(selectedProject?.config?.structure)
|
|
||||||
? selectedProject.config.structure
|
|
||||||
: [])
|
|
||||||
}
|
|
||||||
onChange={setCustomStructure}
|
onChange={setCustomStructure}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -188,6 +188,36 @@ const CreateLyricsWithAi = () => {
|
|||||||
}
|
}
|
||||||
}, [structure]);
|
}, [structure]);
|
||||||
|
|
||||||
|
const sanitizedCustomStructure = useMemo(() => {
|
||||||
|
if (!Array.isArray(customStructure) || customStructure.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS);
|
||||||
|
}, [customStructure]);
|
||||||
|
|
||||||
|
const sanitizedParsedStructure = useMemo(() => {
|
||||||
|
if (!Array.isArray(parsedStructure) || parsedStructure.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return sanitizeStructureList(parsedStructure, CUSTOM_SANITIZE_OPTIONS);
|
||||||
|
}, [parsedStructure]);
|
||||||
|
|
||||||
|
const fallbackProjectStructure = useMemo(() => {
|
||||||
|
const raw = selectedProject?.config?.structure;
|
||||||
|
if (!Array.isArray(raw) || raw.length === 0) return [];
|
||||||
|
return sanitizeStructureList(raw, CUSTOM_SANITIZE_OPTIONS);
|
||||||
|
}, [selectedProject]);
|
||||||
|
|
||||||
|
const baseStructureForCustomizer = useMemo(() => {
|
||||||
|
if (sanitizedCustomStructure.length > 0) return sanitizedCustomStructure;
|
||||||
|
if (sanitizedParsedStructure.length > 0) return sanitizedParsedStructure;
|
||||||
|
return fallbackProjectStructure;
|
||||||
|
}, [
|
||||||
|
sanitizedCustomStructure,
|
||||||
|
sanitizedParsedStructure,
|
||||||
|
fallbackProjectStructure,
|
||||||
|
]);
|
||||||
|
|
||||||
const progress = useMemo(
|
const progress = useMemo(
|
||||||
() => 16 + selectedIndex * 9,
|
() => 16 + selectedIndex * 9,
|
||||||
[selectedIndex]
|
[selectedIndex]
|
||||||
@@ -200,13 +230,8 @@ const CreateLyricsWithAi = () => {
|
|||||||
const resolvedStyle = shouldUseOtherStyle
|
const resolvedStyle = shouldUseOtherStyle
|
||||||
? persistedOtherStyle || undefined
|
? persistedOtherStyle || undefined
|
||||||
: style || undefined;
|
: style || undefined;
|
||||||
const sanitizedCustom =
|
const sanitizedCustom = sanitizedCustomStructure;
|
||||||
Array.isArray(customStructure) && customStructure.length > 0
|
const sanitizedParsed = sanitizedParsedStructure;
|
||||||
? sanitizeStructureList(customStructure, CUSTOM_SANITIZE_OPTIONS)
|
|
||||||
: [];
|
|
||||||
const sanitizedParsed = Array.isArray(parsedStructure)
|
|
||||||
? sanitizeStructureList(parsedStructure)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objective: resolvedObjective,
|
objective: resolvedObjective,
|
||||||
@@ -235,9 +260,9 @@ const CreateLyricsWithAi = () => {
|
|||||||
emotion,
|
emotion,
|
||||||
style,
|
style,
|
||||||
audience,
|
audience,
|
||||||
parsedStructure,
|
sanitizedParsedStructure,
|
||||||
rhymes,
|
rhymes,
|
||||||
customStructure,
|
sanitizedCustomStructure,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isNextDisabled = React.useMemo(() => {
|
const isNextDisabled = React.useMemo(() => {
|
||||||
@@ -358,14 +383,7 @@ const CreateLyricsWithAi = () => {
|
|||||||
key: "customStructure",
|
key: "customStructure",
|
||||||
render: () => (
|
render: () => (
|
||||||
<CustomizeSongStructure
|
<CustomizeSongStructure
|
||||||
baseStructure={
|
baseStructure={baseStructureForCustomizer}
|
||||||
parsedStructure ||
|
|
||||||
(Array.isArray(customStructure)
|
|
||||||
? customStructure
|
|
||||||
: Array.isArray(selectedProject?.config?.structure)
|
|
||||||
? selectedProject.config.structure
|
|
||||||
: [])
|
|
||||||
}
|
|
||||||
onChange={setCustomStructure}
|
onChange={setCustomStructure}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
console.log("🚀 [CreatingLyrics] Lancement de la génération", {
|
console.log("🚀 [CreatingLyrics] Lancement de la génération", {
|
||||||
platform: Platform.OS,
|
platform: Platform.OS,
|
||||||
});
|
});
|
||||||
const sanitizedStructure = sanitizeStructureList(config?.structure);
|
const sanitizedStructure = sanitizeStructureList(
|
||||||
|
config?.structure,
|
||||||
|
CUSTOM_SANITIZE_OPTIONS
|
||||||
|
);
|
||||||
const callable = firebase
|
const callable = firebase
|
||||||
.functions()
|
.functions()
|
||||||
.httpsCallable("lyrics-generateLyrics");
|
.httpsCallable("lyrics-generateLyrics");
|
||||||
@@ -168,7 +171,8 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
|||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
console.log("💾 [CreatingLyrics] Sauvegarde automatique des paroles");
|
console.log("💾 [CreatingLyrics] Sauvegarde automatique des paroles");
|
||||||
const sanitizedStructure = sanitizeStructureList(
|
const sanitizedStructure = sanitizeStructureList(
|
||||||
result?.structure || config?.structure
|
result?.structure || config?.structure,
|
||||||
|
CUSTOM_SANITIZE_OPTIONS
|
||||||
);
|
);
|
||||||
const baseConfig =
|
const baseConfig =
|
||||||
config && typeof config === "object" && !Array.isArray(config)
|
config && typeof config === "object" && !Array.isArray(config)
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
1. Ouvre cette URL dans un navigateur (mets à jour `redirect_uri` si besoin) :
|
1. Ouvre cette URL dans un navigateur (mets à jour `redirect_uri` si besoin) :
|
||||||
```
|
```
|
||||||
https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID.apps.googleusercontent.com&redirect_uri=http://localhost:8081&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.upload&access_type=offline&prompt=consent
|
https://accounts.google.com/o/oauth2/v2/auth?client_id=305598753437-3mn43cs1phacao2f1ctg1dbur7rct0bt.apps.googleusercontent.com&redirect_uri=http://localhost:8081&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.upload&access_type=offline&prompt=consent
|
||||||
```
|
```
|
||||||
2. Connecte-toi au compte YouTube cible et accepte les permissions.
|
2. Connecte-toi au compte YouTube cible et accepte les permissions.
|
||||||
3. Google te redirige vers `http://localhost:8081/?code=...`. Copie la valeur du paramètre `code`.
|
3. Google te redirige vers `http://localhost:8081/?code=...`. Copie la valeur du paramètre `code`.
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST https://oauth2.googleapis.com/token \
|
curl -X POST https://oauth2.googleapis.com/token \
|
||||||
-d client_id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
-d client_id=YOUR_CLIENT_ID \
|
||||||
-d client_secret=YOUR_CLIENT_SECRET \
|
-d client_secret=YOUR_CLIENT_SECRET \
|
||||||
-d code="CODE_RECU" \
|
-d code="CODE_RECU" \
|
||||||
-d grant_type=authorization_code \
|
-d grant_type=authorization_code \
|
||||||
|
|||||||
Reference in New Issue
Block a user