replay musics

This commit is contained in:
2025-11-03 14:01:35 +01:00
parent 460c91f1e6
commit 94a1f1ab16
7 changed files with 143 additions and 27 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ const styles = StyleSheet.create({
backgroundColor: "transparent", backgroundColor: "transparent",
}, },
fallback: { fallback: {
backgroundColor: Palette.glass, backgroundColor: "#7373736e",
borderColor: Palette.ultraLightWhite, borderColor: Palette.ultraLightWhite,
borderWidth: 2, borderWidth: 2,
}, },
+7 -7
View File
@@ -1,12 +1,12 @@
import { View, Text } from "react-native";
import React from "react"; import React from "react";
import AppActionSheet from "../AppActionSheet"; import { Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import AppActionSheet from "../AppActionSheet";
import BlurItemButton from "../BlurItemButton"; import BlurItemButton from "../BlurItemButton";
import { SheetManager } from "react-native-actions-sheet";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const ProfileSettingsModal = () => { const ProfileSettingsModal = () => {
const onClose = async () => { const onClose = async () => {
@@ -27,13 +27,13 @@ const ProfileSettingsModal = () => {
Paramètres Paramètres
</Text> </Text>
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
<BlurItemButton {/* <BlurItemButton
title="Modifier mon profil" title="Modifier mon profil"
onPress={() => { onPress={() => {
onClose(); onClose();
navigate(Routes.EditProfile); navigate(Routes.EditProfile);
}} }}
/> /> */}
<BlurItemButton <BlurItemButton
title="Paramètres" title="Paramètres"
onPress={() => { onPress={() => {
+25
View File
@@ -79,6 +79,9 @@ const DEFAULT_CONTEXT = {
seekBy: noopAsync, seekBy: noopAsync,
stop: noopAsync, stop: noopAsync,
setQueue: noop, setQueue: noop,
isLooping: false,
setLooping: noop,
toggleLooping: noop,
}; };
export const PlayerContext = createContext(DEFAULT_CONTEXT); export const PlayerContext = createContext(DEFAULT_CONTEXT);
@@ -215,6 +218,7 @@ const PlayerProvider = ({ children }) => {
durationMs: 0, durationMs: 0,
}); });
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isLooping, setIsLooping] = useState(false);
const autoPlayRef = useRef(false); const autoPlayRef = useRef(false);
const pendingSeekValueRef = useRef(null); const pendingSeekValueRef = useRef(null);
@@ -295,6 +299,13 @@ const PlayerProvider = ({ children }) => {
run(); run();
}, [player, currentTrack?.id]); }, [player, currentTrack?.id]);
useEffect(() => {
if (!player) return;
try {
player.loop = !!isLooping;
} catch (_err) {}
}, [player, isLooping]);
const play = useCallback( const play = useCallback(
async (trackInput, options = {}) => { async (trackInput, options = {}) => {
const normalized = normalizeTrack(trackInput, options); const normalized = normalizeTrack(trackInput, options);
@@ -455,6 +466,14 @@ const PlayerProvider = ({ children }) => {
} }
}, [player]); }, [player]);
const setLooping = useCallback((value) => {
setIsLooping(!!value);
}, []);
const toggleLooping = useCallback(() => {
setIsLooping((prev) => !prev);
}, []);
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
currentTrack, currentTrack,
@@ -474,6 +493,9 @@ const PlayerProvider = ({ children }) => {
seekBy, seekBy,
stop, stop,
setQueue, setQueue,
isLooping,
setLooping,
toggleLooping,
}), }),
[ [
currentTrack, currentTrack,
@@ -491,6 +513,9 @@ const PlayerProvider = ({ children }) => {
seekTo, seekTo,
shouldHideOnRoute, shouldHideOnRoute,
stop, stop,
isLooping,
setLooping,
toggleLooping,
] ]
); );
+2 -4
View File
@@ -35,7 +35,7 @@ export default function LandingPage() {
}, []); }, []);
const handleLaunchAdventure = useCallback(() => { const handleLaunchAdventure = useCallback(() => {
navigation.navigate(Routes.Register); navigation.navigate(Routes.Login);
}, [navigation]); }, [navigation]);
const { data: video } = useDataFromRef({ const { data: video } = useDataFromRef({
@@ -103,9 +103,7 @@ export default function LandingPage() {
title="Présentation de Musicland" title="Présentation de Musicland"
onPress={() => onPress={() =>
setVideoUrl( setVideoUrl(
Platform.OS === "web" Platform.OS === "web" ? video?.landingWeb : video?.landing
? video?.landingWeb
: video?.landing,
) )
} }
containerStyle={styles.actionButton} containerStyle={styles.actionButton}
+47
View File
@@ -1,4 +1,6 @@
import { Feather } from "@expo/vector-icons";
import useTrackController from "../../hooks/useTrackController"; import useTrackController from "../../hooks/useTrackController";
import usePlayer from "../../hooks/usePlayer";
import { Image as ExpoImage } from "expo-image"; import { Image as ExpoImage } from "expo-image";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { import {
@@ -58,6 +60,7 @@ const MusicDetails = ({ route }) => {
const incrementDoneRef = useRef(false); const incrementDoneRef = useRef(false);
const timerRef = useRef(null); const timerRef = useRef(null);
const hasAutoPlayedRef = useRef(false); const hasAutoPlayedRef = useRef(false);
const { isLooping, setLooping } = usePlayer() || {};
const { data: project } = useDataFromRef({ const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null, ref: projectId ? projectsRef.doc(projectId) : null,
@@ -193,6 +196,7 @@ const MusicDetails = ({ route }) => {
seekTo: seekTrackTo, seekTo: seekTrackTo,
seekBy: seekTrackBy, seekBy: seekTrackBy,
} = useTrackController(trackDescriptor); } = useTrackController(trackDescriptor);
const loopEnabled = isCurrentTrack && !!isLooping;
// Karaoke aligned words (from timestamps) // Karaoke aligned words (from timestamps)
const alignedWords = useMemo(() => { const alignedWords = useMemo(() => {
@@ -325,6 +329,31 @@ const MusicDetails = ({ route }) => {
[durationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo] [durationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo]
); );
const handleToggleLoop = useCallback(async () => {
if (!isCurrentTrack) return;
const next = !loopEnabled;
if (typeof setLooping === "function") {
setLooping(next);
}
if (next) {
try {
await seekTrackTo(0);
if (!isTrackPlaying) {
await resumeTrack();
}
} catch (e) {
console.log("MusicDetails loop toggle error", e?.message);
}
}
}, [
isCurrentTrack,
loopEnabled,
setLooping,
seekTrackTo,
isTrackPlaying,
resumeTrack,
]);
useEffect(() => { useEffect(() => {
if (!autoPlayRequested || hasAutoPlayedRef.current) return; if (!autoPlayRequested || hasAutoPlayedRef.current) return;
if (!trackDescriptor || !songUrl) return; if (!trackDescriptor || !songUrl) return;
@@ -746,6 +775,24 @@ const MusicDetails = ({ route }) => {
<Text style={styles.name}>{artist}</Text> <Text style={styles.name}>{artist}</Text>
</View> </View>
<View style={{ ...Style.containerRow, gap: 16 }}> <View style={{ ...Style.containerRow, gap: 16 }}>
<PressableScale
onPress={handleToggleLoop}
disabled={!isCurrentTrack}
hitSlop={10}
style={{
...size({ size: 32 }),
alignItems: "center",
justifyContent: "center",
opacity: isCurrentTrack ? 1 : 0.4,
}}
contentStyle={{ alignItems: "center", justifyContent: "center" }}
>
<Feather
name="repeat"
size={24}
color={loopEnabled ? Palette.primary : Palette.white}
/>
</PressableScale>
<PressableScale <PressableScale
onPress={async () => { onPress={async () => {
if (!projectId || !currentUID) return; if (!projectId || !currentUID) return;
+52 -4
View File
@@ -1,5 +1,6 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Image as ExpoImage } from "expo-image"; import { Image as ExpoImage } from "expo-image";
import { Feather } from "@expo/vector-icons";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@@ -33,6 +34,7 @@ import {
} from "../../config/firebase"; } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef"; import useDataFromRef from "../../hooks/useDataFromRef";
import useTrackController from "../../hooks/useTrackController"; import useTrackController from "../../hooks/useTrackController";
import usePlayer from "../../hooks/usePlayer";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
@@ -67,6 +69,7 @@ const MusicDetails = ({ route }) => {
const incrementDoneRef = React.useRef(false); const incrementDoneRef = React.useRef(false);
const timerRef = React.useRef(null); const timerRef = React.useRef(null);
const hasAutoPlayedRef = React.useRef(false); const hasAutoPlayedRef = React.useRef(false);
const { isLooping, setLooping } = usePlayer() || {};
const { data: project } = useDataFromRef({ const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null, ref: projectId ? projectsRef.doc(projectId) : null,
@@ -275,6 +278,7 @@ const MusicDetails = ({ route }) => {
} = useTrackController(trackDescriptor); } = useTrackController(trackDescriptor);
const isPlaying = isTrackPlaying; const isPlaying = isTrackPlaying;
const loopEnabled = isCurrentTrack && !!isLooping;
// Karaoke aligned words (from timestamps) // Karaoke aligned words (from timestamps)
const alignedWords = useMemo(() => { const alignedWords = useMemo(() => {
@@ -429,6 +433,31 @@ const MusicDetails = ({ route }) => {
[trackDescriptor, durationMs, isCurrentTrack, ensureLoaded, seekTrackTo] [trackDescriptor, durationMs, isCurrentTrack, ensureLoaded, seekTrackTo]
); );
const handleToggleLoop = useCallback(async () => {
if (!isCurrentTrack) return;
const next = !loopEnabled;
if (typeof setLooping === "function") {
setLooping(next);
}
if (next) {
try {
await seekTrackTo(0);
if (!isPlaying) {
await resumeTrack();
}
} catch (e) {
console.log("MusicDetails.web loop toggle error", e?.message);
}
}
}, [
isCurrentTrack,
loopEnabled,
setLooping,
seekTrackTo,
isPlaying,
resumeTrack,
]);
useEffect(() => { useEffect(() => {
if (!autoPlayRequested || hasAutoPlayedRef.current) { if (!autoPlayRequested || hasAutoPlayedRef.current) {
return; return;
@@ -926,6 +955,27 @@ const MusicDetails = ({ route }) => {
<Text style={styles.name}>{artist}</Text> <Text style={styles.name}>{artist}</Text>
</View> </View>
<View style={{ ...Style.containerRow, gap: 12 }}> <View style={{ ...Style.containerRow, gap: 12 }}>
<PressableScale
onPress={handleToggleLoop}
disabled={!isCurrentTrack}
hitSlop={10}
style={{
...size({ size: 32 }),
alignItems: "center",
justifyContent: "center",
opacity: isCurrentTrack ? 1 : 0.4,
}}
contentStyle={{
alignItems: "center",
justifyContent: "center",
}}
>
<Feather
name="repeat"
size={24}
color={loopEnabled ? Palette.primary : Palette.white}
/>
</PressableScale>
<Pressable <Pressable
onPress={async () => { onPress={async () => {
if (!projectId || !currentUID) return; if (!projectId || !currentUID) return;
@@ -972,9 +1022,7 @@ const MusicDetails = ({ route }) => {
<Pressable onPress={() => handleSeekBySeconds(-10)}> <Pressable onPress={() => handleSeekBySeconds(-10)}>
<RNImage <RNImage
source={icons.forward} source={icons.forward}
style={{ style={{ ...size({ size: 30 }) }}
...size({ size: 30 }),
}}
/> />
</Pressable> </Pressable>
{/* Play / Pause */} {/* Play / Pause */}
@@ -987,7 +1035,7 @@ const MusicDetails = ({ route }) => {
onPress={togglePlay} onPress={togglePlay}
> >
<RNImage <RNImage
resizeMode={"contain"} resizeMode="contain"
source={isPlaying ? icons.pause : icons.play} source={isPlaying ? icons.pause : icons.play}
style={size({ size: 34 })} style={size({ size: 34 })}
/> />
+9 -11
View File
@@ -34,11 +34,11 @@ import Page from "../../layouts/Page";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { push } from "../../navigation/NavigationService"; import { push } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider"; import { useUser } from "../../providers/UserDataProvider";
import { getArtistDisplayName } from "../../utils/artistName";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { gutters, size } from "../../styles/Style"; import Style, { gutters, size } from "../../styles/Style";
import { getArtistDisplayName } from "../../utils/artistName";
import { getProjectLikes, LIKE_TARGET } from "../../utils/likes";
import MusicCard from "../Library/components/MusicCard"; import MusicCard from "../Library/components/MusicCard";
const Profile = () => { const Profile = () => {
const { params } = useRoute(); const { params } = useRoute();
@@ -321,9 +321,7 @@ const Profile = () => {
}); });
const displayedProjects = isSelf ? selfProjects : otherUserProjects; const displayedProjects = isSelf ? selfProjects : otherUserProjects;
const displayedPlaybacksSource = isSelf const displayedPlaybacksSource = isSelf ? selfPlaybacks : otherUserProjects;
? selfPlaybacks
: otherUserProjects;
const displayedPlaybacks = Array.isArray(displayedPlaybacksSource) const displayedPlaybacks = Array.isArray(displayedPlaybacksSource)
? displayedPlaybacksSource.filter((project) => project?.playbackUrl) ? displayedPlaybacksSource.filter((project) => project?.playbackUrl)
: []; : [];
@@ -419,12 +417,12 @@ const Profile = () => {
<Text style={styles.webLoaderText}>{webUploadMessage}</Text> <Text style={styles.webLoaderText}>{webUploadMessage}</Text>
) : null} ) : null}
<Text <Text
style={{ style={{
fontSize: 22, fontSize: 22,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}} }}
> >
{profileTitle} {profileTitle}
</Text> </Text>
</View> </View>