second flow
This commit is contained in:
@@ -2,17 +2,25 @@ import { useEffect, useState } from "react";
|
|||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
|
runOnJS,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
runOnJS,
|
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
|
||||||
import { Palette, Style } from "../styles";
|
import { Palette, Style } from "../styles";
|
||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
|
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
||||||
|
|
||||||
const INITIAL_BOX_SIZE = 6;
|
const INITIAL_BOX_SIZE = 6;
|
||||||
|
|
||||||
export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, seekEnabled = false }) => {
|
export default ({
|
||||||
|
value,
|
||||||
|
maxValue,
|
||||||
|
progress,
|
||||||
|
onSeek,
|
||||||
|
onSeekStart,
|
||||||
|
onSeekEnd,
|
||||||
|
seekEnabled = false,
|
||||||
|
}) => {
|
||||||
const offset = useSharedValue(0);
|
const offset = useSharedValue(0);
|
||||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
||||||
const [layout, setLayout] = useState(null);
|
const [layout, setLayout] = useState(null);
|
||||||
@@ -23,7 +31,7 @@ export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, see
|
|||||||
const pan = Gesture.Pan()
|
const pan = Gesture.Pan()
|
||||||
.enabled(seekEnabled)
|
.enabled(seekEnabled)
|
||||||
.onBegin(() => {
|
.onBegin(() => {
|
||||||
if (seekEnabled && typeof onSeekStart === 'function') {
|
if (seekEnabled && typeof onSeekStart === "function") {
|
||||||
// Notify JS thread that user started seeking (e.g., pause audio)
|
// Notify JS thread that user started seeking (e.g., pause audio)
|
||||||
runOnJS(onSeekStart)();
|
runOnJS(onSeekStart)();
|
||||||
}
|
}
|
||||||
@@ -34,8 +42,8 @@ export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, see
|
|||||||
? offset.value + event.changeX <= 0
|
? offset.value + event.changeX <= 0
|
||||||
? 0
|
? 0
|
||||||
: offset.value + event.changeX >= MAX_VALUE
|
: offset.value + event.changeX >= MAX_VALUE
|
||||||
? MAX_VALUE
|
? MAX_VALUE
|
||||||
: offset.value + event.changeX
|
: offset.value + event.changeX
|
||||||
: offset.value;
|
: offset.value;
|
||||||
|
|
||||||
const newWidth = INITIAL_BOX_SIZE + offset.value;
|
const newWidth = INITIAL_BOX_SIZE + offset.value;
|
||||||
@@ -43,12 +51,13 @@ export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, see
|
|||||||
})
|
})
|
||||||
.onEnd(() => {
|
.onEnd(() => {
|
||||||
if (!seekEnabled || !onSeek || !MAX_VALUE) return;
|
if (!seekEnabled || !onSeek || !MAX_VALUE) return;
|
||||||
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
|
const ratio =
|
||||||
|
MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
|
||||||
// Reanimated -> JS thread bridge
|
// Reanimated -> JS thread bridge
|
||||||
runOnJS(onSeek)(ratio);
|
runOnJS(onSeek)(ratio);
|
||||||
})
|
})
|
||||||
.onFinalize(() => {
|
.onFinalize(() => {
|
||||||
if (seekEnabled && typeof onSeekEnd === 'function') {
|
if (seekEnabled && typeof onSeekEnd === "function") {
|
||||||
runOnJS(onSeekEnd)();
|
runOnJS(onSeekEnd)();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
|
import { Palette, Style } from "../styles";
|
||||||
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
|
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
||||||
|
|
||||||
|
const INITIAL_BOX_SIZE = 6;
|
||||||
|
const HANDLE_SIZE = 20;
|
||||||
|
const HANDLE_TOP_OFFSET = (INITIAL_BOX_SIZE - HANDLE_SIZE) / 2;
|
||||||
|
|
||||||
|
const clamp01 = (value) => Math.min(1, Math.max(0, value));
|
||||||
|
|
||||||
|
const Slider = ({
|
||||||
|
value,
|
||||||
|
maxValue,
|
||||||
|
progress,
|
||||||
|
onSeek,
|
||||||
|
onSeekStart,
|
||||||
|
onSeekEnd,
|
||||||
|
seekEnabled = false,
|
||||||
|
}) => {
|
||||||
|
const [layoutWidth, setLayoutWidth] = useState(0);
|
||||||
|
const [ratio, setRatio] = useState(
|
||||||
|
typeof progress === "number" ? clamp01(progress) : 0,
|
||||||
|
);
|
||||||
|
const draggingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!draggingRef.current && typeof progress === "number") {
|
||||||
|
setRatio(clamp01(progress));
|
||||||
|
}
|
||||||
|
}, [progress]);
|
||||||
|
|
||||||
|
const updateRatioFromX = useCallback(
|
||||||
|
(x) => {
|
||||||
|
if (layoutWidth <= 0) return;
|
||||||
|
const available = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0);
|
||||||
|
if (available <= 0) {
|
||||||
|
setRatio(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clampedX = Math.min(Math.max(x, 0), layoutWidth);
|
||||||
|
const nextRatio = clamp01(clampedX / available);
|
||||||
|
setRatio(nextRatio);
|
||||||
|
},
|
||||||
|
[layoutWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGrant = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!seekEnabled) return;
|
||||||
|
draggingRef.current = true;
|
||||||
|
updateRatioFromX(event?.nativeEvent?.locationX || 0);
|
||||||
|
if (typeof onSeekStart === "function") {
|
||||||
|
onSeekStart();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[seekEnabled, updateRatioFromX, onSeekStart],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMove = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!seekEnabled || !draggingRef.current) return;
|
||||||
|
updateRatioFromX(event?.nativeEvent?.locationX || 0);
|
||||||
|
},
|
||||||
|
[seekEnabled, updateRatioFromX],
|
||||||
|
);
|
||||||
|
|
||||||
|
const finishSeeking = useCallback(() => {
|
||||||
|
if (!seekEnabled || !draggingRef.current) return;
|
||||||
|
draggingRef.current = false;
|
||||||
|
const currentRatio = clamp01(ratio);
|
||||||
|
if (typeof onSeek === "function") {
|
||||||
|
onSeek(currentRatio);
|
||||||
|
}
|
||||||
|
if (typeof onSeekEnd === "function") {
|
||||||
|
onSeekEnd();
|
||||||
|
}
|
||||||
|
}, [seekEnabled, ratio, onSeek, onSeekEnd]);
|
||||||
|
|
||||||
|
const handleLayout = useCallback((event) => {
|
||||||
|
const width = event?.nativeEvent?.layout?.width || 0;
|
||||||
|
if (width !== layoutWidth) {
|
||||||
|
setLayoutWidth(width);
|
||||||
|
}
|
||||||
|
}, [layoutWidth]);
|
||||||
|
|
||||||
|
const maxOffset = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0);
|
||||||
|
const offset = maxOffset * ratio;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.interactionArea,
|
||||||
|
seekEnabled ? styles.pointerEnabled : null,
|
||||||
|
]}
|
||||||
|
onLayout={handleLayout}
|
||||||
|
onStartShouldSetResponderCapture={() => seekEnabled}
|
||||||
|
onStartShouldSetResponder={() => seekEnabled}
|
||||||
|
onMoveShouldSetResponder={() => seekEnabled}
|
||||||
|
onResponderGrant={handleGrant}
|
||||||
|
onResponderMove={handleMove}
|
||||||
|
onResponderRelease={finishSeeking}
|
||||||
|
onResponderTerminate={finishSeeking}
|
||||||
|
>
|
||||||
|
<View style={styles.sliderTrack}>
|
||||||
|
<View style={[styles.box, { width: INITIAL_BOX_SIZE + offset }]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={["#F94697", "#7023F7"]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
style={{ flex: 1, borderRadius: 20 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.sliderHandle, { left: offset }]} />
|
||||||
|
</View>
|
||||||
|
<View style={{ ...Style.containerSpaceBetween, marginTop: 10 }}>
|
||||||
|
<Text style={styles.time}>{value}</Text>
|
||||||
|
<Text style={styles.time}>{maxValue}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
interactionArea: {
|
||||||
|
width: "100%",
|
||||||
|
height: HANDLE_SIZE,
|
||||||
|
justifyContent: "center",
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
pointerEnabled: {
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
sliderTrack: {
|
||||||
|
width: "100%",
|
||||||
|
height: INITIAL_BOX_SIZE,
|
||||||
|
backgroundColor: "#0F0C19",
|
||||||
|
borderRadius: 25,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
box: {
|
||||||
|
height: INITIAL_BOX_SIZE,
|
||||||
|
borderRadius: 20,
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
sliderHandle: {
|
||||||
|
width: HANDLE_SIZE,
|
||||||
|
height: HANDLE_SIZE,
|
||||||
|
backgroundColor: "#f8f9ff",
|
||||||
|
borderRadius: HANDLE_SIZE / 2,
|
||||||
|
position: "absolute",
|
||||||
|
top: HANDLE_TOP_OFFSET,
|
||||||
|
zIndex: 2,
|
||||||
|
borderWidth: 4,
|
||||||
|
borderColor: "#9B4DFF",
|
||||||
|
shadowColor: "#8951FC",
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 3,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.17,
|
||||||
|
shadowRadius: 3.05,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Slider;
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Dimensions, FlatList, View } from "react-native";
|
||||||
|
import { background } from "../../assets";
|
||||||
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
|
import firebase from "../../config/firebase";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
|
import { Routes } from "../../navigation";
|
||||||
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
|
import { gutters } from "../../styles";
|
||||||
|
import ChooseGenre from "./ChooseGenre";
|
||||||
|
import ChooseInstruments from "./ChooseInstruments";
|
||||||
|
import ChooseRhythm from "./ChooseRhythm";
|
||||||
|
import CustomizeVoice from "./CustomizeVoice";
|
||||||
|
|
||||||
|
const { width: windowWidth } = Dimensions.get("window");
|
||||||
|
|
||||||
|
const ComposeSong = () => {
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const [progress, setProgress] = useState(18);
|
||||||
|
const [parentLayout, setParentLayout] = useState(null);
|
||||||
|
const containerWidth = parentLayout?.width || windowWidth || 1;
|
||||||
|
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||||
|
|
||||||
|
const [genres, setGenres] = useState([]);
|
||||||
|
const [voice, setVoice] = useState({});
|
||||||
|
const [instruments, setInstruments] = useState([]);
|
||||||
|
const [rhythm, setRhythm] = useState(null);
|
||||||
|
|
||||||
|
const isStepValid = useMemo(() => {
|
||||||
|
switch (selectedIndex) {
|
||||||
|
case 0:
|
||||||
|
return Array.isArray(genres) && genres.length > 0;
|
||||||
|
case 1:
|
||||||
|
return !!(voice && typeof voice === "object" && voice.base);
|
||||||
|
case 2:
|
||||||
|
return Array.isArray(instruments) && instruments.length > 0;
|
||||||
|
case 3:
|
||||||
|
return !!rhythm;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}, [selectedIndex, genres, voice, instruments, rhythm]);
|
||||||
|
|
||||||
|
const musicConfig = useMemo(() => {
|
||||||
|
let lyricsArr = [];
|
||||||
|
if (Array.isArray(selectedProject?.lyrics)) {
|
||||||
|
lyricsArr = selectedProject.lyrics.map((s) => ({
|
||||||
|
type: (s?.type || "").toLowerCase(),
|
||||||
|
lyrics: s?.lyrics || "",
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const c = selectedProject?.lyrics?.couplet;
|
||||||
|
const r = selectedProject?.lyrics?.refrain;
|
||||||
|
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
|
||||||
|
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
|
||||||
|
}
|
||||||
|
const voiceArray = Object.entries(voice || {})
|
||||||
|
.filter(([, v]) => typeof v === "string" && v.trim())
|
||||||
|
.map(([category, value]) => ({ category, value }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: selectedProject?.title || "",
|
||||||
|
lyrics: lyricsArr,
|
||||||
|
genres: Array.isArray(genres) ? genres : [],
|
||||||
|
voice: voiceArray,
|
||||||
|
instruments: Array.isArray(instruments) ? instruments : [],
|
||||||
|
tempo: rhythm || undefined,
|
||||||
|
projectId: selectedProjectId || undefined,
|
||||||
|
};
|
||||||
|
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
|
||||||
|
|
||||||
|
const steps = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: "genres",
|
||||||
|
render: () => <ChooseGenre selected={genres} setSelected={setGenres} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "voice",
|
||||||
|
render: () => (
|
||||||
|
<CustomizeVoice selected={voice} setSelected={setVoice} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "instruments",
|
||||||
|
render: () => (
|
||||||
|
<ChooseInstruments
|
||||||
|
selected={instruments}
|
||||||
|
setSelected={setInstruments}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "rhythm",
|
||||||
|
render: () => (
|
||||||
|
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[genres, voice, instruments, rhythm]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextProgress = 18 + selectedIndex * 9;
|
||||||
|
if (nextProgress !== progress) {
|
||||||
|
setProgress(nextProgress);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
scrollRef.current?.scrollToIndex?.({
|
||||||
|
index: selectedIndex,
|
||||||
|
animated: true,
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}, [selectedIndex, progress, containerWidth]);
|
||||||
|
|
||||||
|
const getItemLayout = useCallback(
|
||||||
|
(_data, index) => ({
|
||||||
|
length: containerWidth,
|
||||||
|
offset: containerWidth * index,
|
||||||
|
index,
|
||||||
|
}),
|
||||||
|
[containerWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPressNext = async () => {
|
||||||
|
if (selectedIndex === steps.length - 1) {
|
||||||
|
try {
|
||||||
|
if (selectedProjectId) {
|
||||||
|
await updateProjectData({
|
||||||
|
musicConfig: {
|
||||||
|
title: musicConfig?.title || "",
|
||||||
|
lyrics: Array.isArray(musicConfig?.lyrics)
|
||||||
|
? musicConfig.lyrics
|
||||||
|
: [],
|
||||||
|
genres: Array.isArray(musicConfig?.genres)
|
||||||
|
? musicConfig.genres
|
||||||
|
: [],
|
||||||
|
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
|
||||||
|
instruments: Array.isArray(musicConfig?.instruments)
|
||||||
|
? musicConfig.instruments
|
||||||
|
: [],
|
||||||
|
tempo: musicConfig?.tempo || "",
|
||||||
|
},
|
||||||
|
musicStatus: null,
|
||||||
|
sunoTaskId: firebase.firestore.FieldValue.delete(),
|
||||||
|
musicUrls: firebase.firestore.FieldValue.delete(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
navigate(Routes.GeneratingSong, { config: musicConfig });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPressBack = () => {
|
||||||
|
if (selectedIndex > 0) {
|
||||||
|
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
||||||
|
} else {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||||
|
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
|
||||||
|
<View style={{ flex: 1, paddingBottom: gutters, gap: 48 }}>
|
||||||
|
<View
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onLayout={(event) => setParentLayout(event.nativeEvent.layout)}
|
||||||
|
>
|
||||||
|
<FlatList
|
||||||
|
ref={scrollRef}
|
||||||
|
data={steps}
|
||||||
|
keyExtractor={(item) => item.key}
|
||||||
|
horizontal
|
||||||
|
pagingEnabled
|
||||||
|
scrollEnabled={false}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
initialScrollIndex={selectedIndex}
|
||||||
|
getItemLayout={getItemLayout}
|
||||||
|
style={{ width: containerWidth }}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: containerWidth,
|
||||||
|
height: parentLayout?.height,
|
||||||
|
paddingHorizontal: gutters,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.render()}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{selectedIndex !== steps.length && (
|
||||||
|
<GradientButton
|
||||||
|
title={selectedIndex === steps.length - 1 ? "Générer" : "Suivant"}
|
||||||
|
onPress={onPressNext}
|
||||||
|
disabled={!isStepValid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComposeSong;
|
||||||
@@ -2,14 +2,16 @@ import { useIsFocused } from "@react-navigation/native";
|
|||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Alert, Image, Platform, Text, View } from "react-native";
|
import { Image, Platform, Text, View } from "react-native";
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { ai, background } from "../../assets";
|
import { ai, background } from "../../assets";
|
||||||
|
import AppAlert from "../../components/Alert";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import ProgressBar from "../../components/ProgressBar";
|
import ProgressBar from "../../components/ProgressBar";
|
||||||
import firebase, { projectsRef } from "../../config/firebase";
|
import firebase, { projectsRef } from "../../config/firebase";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { navigate } from "../../navigation/NavigationService";
|
import { navigate } from "../../navigation/NavigationService";
|
||||||
@@ -36,7 +38,7 @@ const GeneratingSong = () => {
|
|||||||
const totalMs = 8 * 60 * 1000;
|
const totalMs = 8 * 60 * 1000;
|
||||||
const clearTimer = () => {
|
const clearTimer = () => {
|
||||||
if (progressTimerRef.current) {
|
if (progressTimerRef.current) {
|
||||||
clearInterval(progressTimerRef.current);
|
global.clearInterval(progressTimerRef.current);
|
||||||
progressTimerRef.current = null;
|
progressTimerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -80,7 +82,7 @@ const GeneratingSong = () => {
|
|||||||
|
|
||||||
update();
|
update();
|
||||||
clearTimer();
|
clearTimer();
|
||||||
progressTimerRef.current = setInterval(update, 1000);
|
progressTimerRef.current = global.setInterval(update, 1000);
|
||||||
return () => clearTimer();
|
return () => clearTimer();
|
||||||
}, [selectedProject?.musicStatus, selectedProject?.generationStartAt]);
|
}, [selectedProject?.musicStatus, selectedProject?.generationStartAt]);
|
||||||
|
|
||||||
@@ -163,11 +165,11 @@ const GeneratingSong = () => {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// Keep locked while status transitions to GENERATING; will be prevented by guards
|
// Keep locked while status transitions to GENERATING; will be prevented by guards
|
||||||
setTimeout(() => {
|
global.setTimeout(() => {
|
||||||
callingRef.current = false;
|
callingRef.current = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [startMusicGeneration]);
|
||||||
|
|
||||||
// Trigger generation only when focused; ask once while idle
|
// Trigger generation only when focused; ask once while idle
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -183,7 +185,7 @@ const GeneratingSong = () => {
|
|||||||
|
|
||||||
if (titleOk && isIdle && !askedRef.current) {
|
if (titleOk && isIdle && !askedRef.current) {
|
||||||
askedRef.current = true;
|
askedRef.current = true;
|
||||||
Alert.alert("Attention", "Une génération va être lancée. Continuer ?", [
|
AppAlert("Attention", "Une génération va être lancée. Continuer ?", [
|
||||||
{
|
{
|
||||||
text: "Non",
|
text: "Non",
|
||||||
style: "cancel",
|
style: "cancel",
|
||||||
@@ -194,7 +196,12 @@ const GeneratingSong = () => {
|
|||||||
{ text: "Oui", onPress: () => startMusicGenerationOnce() },
|
{ text: "Oui", onPress: () => startMusicGenerationOnce() },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, [isFocused, selectedProject?.title, selectedProject?.musicStatus]);
|
}, [
|
||||||
|
isFocused,
|
||||||
|
selectedProject?.title,
|
||||||
|
selectedProject?.musicStatus,
|
||||||
|
startMusicGenerationOnce,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||||
@@ -227,7 +234,11 @@ const GeneratingSong = () => {
|
|||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={ai.theo}
|
source={ai.theo}
|
||||||
style={{ width: "100%", height: "100%", right: -10 }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: isWeb ? 300 : "100%",
|
||||||
|
right: -10,
|
||||||
|
}}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* global setInterval, clearInterval */
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { useAudioPlayer } from "expo-audio";
|
import { useAudioPlayer } from "expo-audio";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
@@ -51,11 +52,18 @@ const SongReady = () => {
|
|||||||
|
|
||||||
// Sync progression depuis les players
|
// Sync progression depuis les players
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// expo-audio returns seconds on native, but on web values can be milliseconds
|
||||||
|
const toMs = (t) => {
|
||||||
|
const n = Number(t || 0);
|
||||||
|
if (!isFinite(n) || n <= 0) return 0;
|
||||||
|
return Platform.OS === "web" ? n : n * 1000;
|
||||||
|
};
|
||||||
|
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
const d0 = (player0?.duration || 0) * 1000;
|
const d0 = toMs(player0?.duration);
|
||||||
const p0 = (player0?.currentTime || 0) * 1000;
|
const p0 = toMs(player0?.currentTime);
|
||||||
const d1 = (player1?.duration || 0) * 1000;
|
const d1 = toMs(player1?.duration);
|
||||||
const p1 = (player1?.currentTime || 0) * 1000;
|
const p1 = toMs(player1?.currentTime);
|
||||||
setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } });
|
setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } });
|
||||||
setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing });
|
setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing });
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -103,6 +111,7 @@ const SongReady = () => {
|
|||||||
const pos = Math.floor(dur * ratio);
|
const pos = Math.floor(dur * ratio);
|
||||||
const player = idx === 0 ? player0 : player1;
|
const player = idx === 0 ? player0 : player1;
|
||||||
if (player && dur > 0) {
|
if (player && dur > 0) {
|
||||||
|
// seekTo expects seconds
|
||||||
await player.seekTo?.(Math.floor((pos || 0) / 1000));
|
await player.seekTo?.(Math.floor((pos || 0) / 1000));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { ActivityIndicator, Text, View } from "react-native";
|
|
||||||
import React from "react";
|
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
|
||||||
import { background } from "../../assets";
|
|
||||||
import Page from "../../layouts/Page";
|
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
|
||||||
import { gutters, Palette, Style } from "../../styles";
|
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
|
||||||
import { Routes } from "../../navigation";
|
|
||||||
import GradientButton from "../../components/GradientButton";
|
|
||||||
import * as ImagePicker from "expo-image-picker";
|
|
||||||
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
|
|
||||||
import { useUserData } from "../../providers/UserDataProvider";
|
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
|
||||||
import firebase, { tasksRef } from "../../config/firebase";
|
|
||||||
import { Image as ExpoImage } from "expo-image";
|
import { Image as ExpoImage } from "expo-image";
|
||||||
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import React from "react";
|
||||||
|
import { ActivityIndicator, Text, View } from "react-native";
|
||||||
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
|
||||||
|
import { background } from "../../assets";
|
||||||
|
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||||
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
|
import firebase, { tasksRef } from "../../config/firebase";
|
||||||
|
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
|
import { Routes } from "../../navigation";
|
||||||
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
|
import { useUserData } from "../../providers/UserDataProvider";
|
||||||
|
import { gutters, Palette, Style } from "../../styles";
|
||||||
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
|
|
||||||
const PhotoCover = () => {
|
const PhotoCover = () => {
|
||||||
const { selectedProject, selectedProjectId, updateProjectData } =
|
const { selectedProject, selectedProjectId, updateProjectData } =
|
||||||
@@ -81,7 +82,12 @@ const PhotoCover = () => {
|
|||||||
priority="high"
|
priority="high"
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
transition={120}
|
transition={120}
|
||||||
style={{ width: "100%", height: 300, borderRadius: 20 }}
|
style={{
|
||||||
|
width: isWeb ? 300 : "100%",
|
||||||
|
height: 300,
|
||||||
|
borderRadius: 20,
|
||||||
|
alignSelf: "center",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
import {
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
Image,
|
|
||||||
ActivityIndicator,
|
|
||||||
Alert,
|
|
||||||
setTimeout,
|
|
||||||
} from "react-native";
|
|
||||||
import { Image as ExpoImage } from "expo-image";
|
import { Image as ExpoImage } from "expo-image";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { ActivityIndicator, Alert, Text, View } from "react-native";
|
||||||
import Page from "../../layouts/Page";
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
import { background, icons, img } from "../../assets";
|
import { background, icons } from "../../assets";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
|
||||||
import { gutters, Palette, Style } from "../../styles";
|
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import { Routes } from "../../navigation";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import firebase, { tasksRef } from "../../config/firebase";
|
import firebase, { tasksRef } from "../../config/firebase";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
|
import { Routes } from "../../navigation";
|
||||||
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
import { gutters, Palette, Style } from "../../styles";
|
||||||
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
|
|
||||||
const PouchReady = () => {
|
const PouchReady = () => {
|
||||||
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||||
@@ -101,12 +95,17 @@ const PouchReady = () => {
|
|||||||
priority="high"
|
priority="high"
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
transition={120}
|
transition={120}
|
||||||
style={{ width: "100%", height: 300, borderRadius: 20 }}
|
style={{
|
||||||
|
width: isWeb ? 300 : "100%",
|
||||||
|
height: 300,
|
||||||
|
borderRadius: 20,
|
||||||
|
alignSelf: "center",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: isWeb ? 300 : "100%",
|
||||||
height: 300,
|
height: 300,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: "#00000040",
|
backgroundColor: "#00000040",
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { View, Image } from "react-native";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import { Image, View } from "react-native";
|
||||||
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
import { background, img } from "../../assets";
|
import { background, img } from "../../assets";
|
||||||
import Page from "../../layouts/Page";
|
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
|
||||||
import { gutters, Style } from "../../styles";
|
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
|
import { isWeb } from "../../hooks/useLayoutType";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import { useUserData } from "../../providers/UserDataProvider";
|
import { useUserData } from "../../providers/UserDataProvider";
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
import { gutters, Style } from "../../styles";
|
||||||
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
|
|
||||||
const ValidateCover = () => {
|
const ValidateCover = () => {
|
||||||
const { selectedProject, updateProjectData } = useUserData();
|
const { selectedProject, updateProjectData } = useUserData();
|
||||||
@@ -57,10 +58,11 @@ const ValidateCover = () => {
|
|||||||
<Image
|
<Image
|
||||||
source={img.placeholder}
|
source={img.placeholder}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: isWeb ? 300 : "100%",
|
||||||
height: 300,
|
height: 300,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
transform: [{ rotateY: "180deg" }],
|
transform: [{ rotateY: "180deg" }],
|
||||||
|
alignSelf: "center",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
@@ -68,7 +70,12 @@ const ValidateCover = () => {
|
|||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: selectedProject?.cover?.result }}
|
source={{ uri: selectedProject?.cover?.result }}
|
||||||
style={{ width: "100%", height: "100%", borderRadius: 20 }}
|
style={{
|
||||||
|
width: isWeb ? 300 : "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 20,
|
||||||
|
alignSelf: "center",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{/*<View*/}
|
{/*<View*/}
|
||||||
|
|||||||
Reference in New Issue
Block a user