web playlist modal

This commit is contained in:
2025-10-02 16:48:35 +02:00
parent 887358645e
commit ea421d8eff
2 changed files with 386 additions and 17 deletions
+39 -17
View File
@@ -9,10 +9,10 @@ import {
View,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import SwiperFlatList from "react-native-swiper-flatlist";
import { icons } from "../../assets";
import { arrayUnion, playlistsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import { useUserData } from "../../providers/UserDataProvider";
import { createPlaylist } from "../../screens/Library/Playlists/playlist";
import { Palette } from "../../styles";
@@ -33,12 +33,22 @@ const PlaylistModal = (props) => {
const startAtCreate = props?.payload?.startAtCreate || false;
const { setTooltip } = useMinuit();
const sanitizedPlaylists = useMemo(
() => (Array.isArray(userPlaylists) ? userPlaylists : []),
[userPlaylists],
);
const isDuplicateName = useMemo(() => {
const lower = (playlist || "").trim().toLowerCase();
if (!lower) return false;
const list = Array.isArray(userPlaylists) ? userPlaylists : [];
return list.some((p) => (p?.name || "").trim().toLowerCase() === lower);
}, [playlist, JSON.stringify(userPlaylists)]);
return sanitizedPlaylists.some(
(p) => (p?.name || "").trim().toLowerCase() === lower,
);
}, [playlist, sanitizedPlaylists]);
const hideSheet = () => {
SheetManager.hide("Playlist");
};
return (
<AppActionSheet id="Playlist">
@@ -60,8 +70,10 @@ const PlaylistModal = (props) => {
>
Ajouter ce titre à une playlist
</Text>
<FlatList
data={Array.isArray(userPlaylists) ? userPlaylists : []}
data={sanitizedPlaylists}
keyExtractor={(item, index) => item?.id || `playlist-${index}`}
contentContainerStyle={{ gap: 20 }}
ListHeaderComponent={
<Pressable
@@ -70,7 +82,7 @@ const PlaylistModal = (props) => {
gap: 10,
}}
onPress={() =>
scrollRef.current.scrollToIndex({
scrollRef.current?.scrollToIndex({
index: 1,
animated: true,
})
@@ -96,6 +108,7 @@ const PlaylistModal = (props) => {
/>
)}
/>
<GradientButton
title="Valider"
containerStyle={{
@@ -113,9 +126,9 @@ const PlaylistModal = (props) => {
musics: arrayUnion(projectId),
updatedAt: new Date(),
});
const addedPlaylist = (
Array.isArray(userPlaylists) ? userPlaylists : []
).find((p) => p?.id === selectedId);
const addedPlaylist = sanitizedPlaylists.find(
(p) => p?.id === selectedId,
);
const name = addedPlaylist?.name || "la playlist";
setTooltip({
type: "success",
@@ -129,18 +142,19 @@ const PlaylistModal = (props) => {
text: e?.message || "Ajout impossible",
});
} finally {
SheetManager.hide("Playlist");
hideSheet();
}
}}
/>
</View>
</View>
<View style={{ width, paddingHorizontal: 14 }}>
<View style={{ gap: 20 }}>
{!startAtCreate && (
<Pressable
onPress={() => {
scrollRef.current.scrollToIndex({
scrollRef.current?.scrollToIndex({
index: 0,
animated: true,
});
@@ -158,12 +172,14 @@ const PlaylistModal = (props) => {
</Text>
</Pressable>
)}
<View style={{ ...Style.containerCenter, gap: 14 }}>
<Text
style={{
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
}}
>
Nommer la playlist
@@ -197,6 +213,7 @@ const PlaylistModal = (props) => {
</Text>
)}
</View>
<GradientButton
title={isCreating ? "Création..." : "Créer la playlist"}
containerStyle={{
@@ -211,10 +228,9 @@ const PlaylistModal = (props) => {
setIsCreating(true);
const name = playlist.trim();
const lower = name.toLowerCase();
// Unicité du nom pour l'utilisateur courant (insensible à la casse)
const exists = (
Array.isArray(userPlaylists) ? userPlaylists : []
).some((p) => (p?.name || "").trim().toLowerCase() === lower);
const exists = sanitizedPlaylists.some(
(p) => (p?.name || "").trim().toLowerCase() === lower,
);
if (exists) {
setTooltip({
type: "error",
@@ -222,6 +238,7 @@ const PlaylistModal = (props) => {
});
return;
}
await createPlaylist({
createdBy: currentUID,
name,
@@ -231,15 +248,20 @@ const PlaylistModal = (props) => {
createdAt: new Date(),
updatedAt: new Date(),
});
setTooltip({
type: "success",
text: props?.payload?.projectId
? `Playlist “${name}” créée et musique ajoutée`
: `Playlist “${name}” créée`,
});
setPlaylist("");
SheetManager.hide("Playlist");
scrollRef.current.scrollToIndex({ index: 0, animated: true });
scrollRef.current?.scrollToIndex({
index: 0,
animated: true,
});
hideSheet();
} catch (e) {
console.log(e);
setTooltip({
+347
View File
@@ -0,0 +1,347 @@
import { Portal } from "@gorhom/portal";
import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useState } from "react";
import {
FlatList,
Image,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { SheetManager, useProviderContext } from "react-native-actions-sheet";
import { actionSheetEventManager } from "react-native-actions-sheet/dist/src/eventmanager";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import { icons } from "../../assets";
import { arrayUnion, playlistsRef } from "../../config/firebase";
import { useUserData } from "../../providers/UserDataProvider";
import { createPlaylist } from "../../screens/Library/Playlists/playlist";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import AppCheckbox from "../AppCheckbox";
import GradientButton from "../GradientButton";
const PlaylistModal = (props) => {
const [selectedId, setSelectedId] = useState("");
const [playlist, setPlaylist] = useState("");
const [isCreating, setIsCreating] = useState(false);
const { currentUID, userPlaylists = [] } = useUserData();
const { setTooltip } = useMinuit();
const startAtCreate = props?.payload?.startAtCreate || false;
const [mode, setMode] = useState(startAtCreate ? "create" : "select");
const [visible, setVisible] = useState(true);
const providerContext = useProviderContext();
useEffect(() => {
setMode(startAtCreate ? "create" : "select");
}, [startAtCreate]);
const sanitizedPlaylists = useMemo(
() => (Array.isArray(userPlaylists) ? userPlaylists : []),
[userPlaylists]
);
const isDuplicateName = useMemo(() => {
const lower = (playlist || "").trim().toLowerCase();
if (!lower) return false;
return sanitizedPlaylists.some(
(p) => (p?.name || "").trim().toLowerCase() === lower
);
}, [playlist, sanitizedPlaylists]);
useEffect(() => {
setVisible(true);
}, [props?.sheetId, props?.payload]);
const hideModal = () => {
setVisible(false);
props?.payload?.onClose?.();
const sheetId = props?.sheetId || "Playlist";
if (props?.sheetId) {
SheetManager.hide(sheetId, { context: providerContext });
// Web modal is rendered outside an actual ActionSheet, so we broadcast the
// close event manually to let the SheetProvider unmount it.
actionSheetEventManager.publish(`onclose_${sheetId}`, undefined, providerContext);
}
};
const handleValidateSelection = async () => {
if (!selectedId) return;
try {
const projectId = props?.payload?.projectId;
if (projectId) {
await playlistsRef.doc(selectedId).update({
musics: arrayUnion(projectId),
updatedAt: new Date(),
});
const addedPlaylist = sanitizedPlaylists.find(
(p) => p?.id === selectedId
);
const name = addedPlaylist?.name || "la playlist";
setTooltip({ type: "success", text: `Ajouté à “${name}` });
}
} catch (e) {
console.log("Add to playlist error", e?.message);
setTooltip({
type: "error",
text: e?.message || "Ajout impossible",
});
} finally {
hideModal();
}
};
const handleCreatePlaylist = async () => {
if (!playlist?.trim()?.length || !currentUID) return;
try {
setIsCreating(true);
const name = playlist.trim();
const lower = name.toLowerCase();
const exists = sanitizedPlaylists.some(
(p) => (p?.name || "").trim().toLowerCase() === lower
);
if (exists) {
setTooltip({
type: "error",
text: "Ce nom de playlist existe déjà",
});
return;
}
await createPlaylist({
createdBy: currentUID,
name,
musics: props?.payload?.projectId ? [props.payload.projectId] : [],
createdAt: new Date(),
updatedAt: new Date(),
});
setTooltip({
type: "success",
text: props?.payload?.projectId
? `Playlist “${name}” créée et musique ajoutée`
: `Playlist “${name}” créée`,
});
setPlaylist("");
setSelectedId("");
setMode("select");
hideModal();
} catch (e) {
console.log(e);
setTooltip({
type: "error",
text: e?.message || "Création impossible",
});
} finally {
setIsCreating(false);
}
};
const renderSelectStep = () => (
<View style={{ gap: 24 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
}}
>
Ajouter ce titre à une playlist
</Text>
<FlatList
data={sanitizedPlaylists}
keyExtractor={(item, index) => item?.id || `playlist-${index}`}
contentContainerStyle={{ gap: 20 }}
style={{ maxHeight: 320 }}
ListHeaderComponent={
<Pressable
style={{
...Style.containerRow,
gap: 10,
alignSelf: "center",
}}
onPress={() => {
setPlaylist("");
setMode("create");
}}
>
<Image source={icons.add} style={size({ size: 15 })} />
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
}}
>
Créer une nouvelle playlist
</Text>
</Pressable>
}
renderItem={({ item }) => (
<AppCheckbox
label={item?.name || "Sans nom"}
onPress={() => setSelectedId(item?.id)}
selected={selectedId === item?.id}
/>
)}
ListEmptyComponent={
<Text
style={{
fontSize: 14,
color: Palette.white,
opacity: 0.8,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
}}
>
Aucune playlist pour le moment
</Text>
}
/>
<GradientButton
title="Valider"
containerStyle={{
width: "80%",
alignSelf: "center",
opacity: selectedId ? 1 : 0.5,
}}
disabled={!selectedId}
onPress={handleValidateSelection}
/>
</View>
);
const renderCreateStep = () => (
<View style={{ gap: 24 }}>
{!startAtCreate && (
<Pressable
onPress={() => {
setMode("select");
setPlaylist("");
}}
>
<Text
style={{
fontSize: 12,
color: Palette.gray,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
}}
>
Retour
</Text>
</Pressable>
)}
<View style={{ ...Style.containerCenter, gap: 14 }}>
<Text
style={{
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
}}
>
Nommer la playlist
</Text>
<TextInput
autoFocus={true}
style={{
paddingVertical: 6,
borderBottomWidth: 1,
borderColor: Palette.white,
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
minWidth: "40%",
maxWidth: "80%",
textAlign: "center",
}}
value={playlist}
onChangeText={setPlaylist}
/>
{isDuplicateName && (
<Text
style={{
fontSize: 12,
color: Palette.red,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
}}
>
Ce nom de playlist existe déjà
</Text>
)}
</View>
<GradientButton
title={isCreating ? "Création..." : "Créer la playlist"}
containerStyle={{
width: "80%",
alignSelf: "center",
opacity: playlist?.trim()?.length ? 1 : 0.5,
}}
disabled={!playlist?.trim()?.length || isCreating}
onPress={handleCreatePlaylist}
/>
</View>
);
if (!visible) return null;
return (
<Portal>
<View style={styles.overlayContainer}>
<BlurView
intensity={25}
tint="dark"
style={styles.blurOverlay}
pointerEvents="none"
/>
<Pressable style={styles.backdrop} onPress={hideModal} />
<View style={styles.modalWrapper}>
<View style={styles.modalContent}>
{mode === "select" ? renderSelectStep() : renderCreateStep()}
</View>
</View>
</View>
</Portal>
);
};
const styles = StyleSheet.create({
overlayContainer: {
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
zIndex: 1000,
},
blurOverlay: {
...StyleSheet.absoluteFillObject,
zIndex: 0,
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(10, 5, 20, 0.55)",
zIndex: 1,
cursor: "pointer",
},
modalWrapper: {
width: "90%",
maxWidth: 520,
borderRadius: 24,
overflow: "hidden",
zIndex: 2,
},
modalContent: {
paddingVertical: 32,
paddingHorizontal: 28,
backgroundColor: "rgba(30, 23, 38, 0.92)",
gap: 24,
},
});
export default PlaylistModal;