Change main page and modify flows

This commit is contained in:
Thomas Demirdjian
2025-09-02 11:45:27 +02:00
parent 8b7980308a
commit 98367af0b7
47 changed files with 3695 additions and 1020 deletions
+2 -1
View File
@@ -114,7 +114,8 @@
"recordAudioAndroid": true
}
],
"expo-audio"
"expo-audio",
"expo-video"
],
"extra": {
"eas": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

+1 -1
View File
@@ -2,7 +2,7 @@ const admin = require("firebase-admin");
const serviceAccount = require("./serviceAccountKey.json");
const { googleAI } = require("@genkit-ai/googleai");
const { GEMINI_API_KEY } = require("./config/keys");
const { genkit, z } = require("genkit");
const { genkit } = require("genkit");
const axios = require("axios");
admin.initializeApp({
+1976 -62
View File
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -14,19 +14,15 @@
},
"main": "index.js",
"dependencies": {
"@genkit-ai/googleai": "^1.16.0",
"@genkit-ai/googleai": "^1.18.0",
"axios": "^1.6.0",
"firebase-admin": "^12.1.0",
"firebase-functions": "^5.0.0",
"genkit": "^1.16.0",
"zod": "3.23.8"
"genkit": "^1.18.0"
},
"devDependencies": {
"eslint": "^8.15.0",
"eslint-config-google": "^0.14.0"
},
"overrides": {
"zod": "3.23.8"
},
"private": true
}
+3 -1
View File
@@ -18,6 +18,7 @@
"@expo/config-plugins": "~9.0.0",
"@expo/metro-runtime": "~4.0.1",
"@expo/react-native-action-sheet": "^4.0.1",
"@expo/vector-icons": "~14.0.4",
"@gorhom/bottom-sheet": "^4",
"@gorhom/portal": "^1.0.14",
"@legendapp/motion": "^2.2.1",
@@ -36,7 +37,6 @@
"deprecated-react-native-prop-types": "^3.0.1",
"expo": "~52.0.47",
"expo-audio": "~0.3.5",
"expo-av": "~15.0.2",
"expo-blur": "~14.0.3",
"expo-build-properties": "~0.13.3",
"expo-camera": "~16.0.18",
@@ -56,6 +56,7 @@
"expo-splash-screen": "~0.29.24",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.9",
"expo-video": "~2.0.6",
"firebase": "^9.20.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
@@ -78,6 +79,7 @@
"react-native-reanimated-carousel": "^4.0.3",
"react-native-responsive-dimensions": "^3.1.1",
"react-native-safe-area-context": "4.12.0",
"react-native-sortables": "^1.8.0",
"react-native-store-review": "^0.3.0",
"react-native-super-grid": "^5.0.0",
"react-native-svg": "15.8.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

+57 -56
View File
@@ -10,20 +10,6 @@ import ribbon from "./UI/tabs/ribbon.png";
import mic from "./UI/tabs/mic.png";
import person from "./UI/tabs/person.png";
export const tabs = {
home,
tasks,
settings,
quotes,
chat,
design,
addTab,
albums,
ribbon,
mic,
person,
};
import bell from "./UI/bell.png";
import sort from "./UI/sort.png";
@@ -86,9 +72,66 @@ import hitParadeLogo from "./icons/hitParadeLogo.png";
import calendar from "./icons/calendar.png";
import coin from "./icons/coin.png";
import triangle from "./art/triangle.png";
import circle from "./art/circle.png";
import square from "./art/square.png";
import gradientTriangle from "./art/gradientTriangle.png";
import threeCoins from "./art/threeCoins.png";
import boost from "./art/boost.png";
import boostBig from "./art/boostBig.png";
import welcome from "./tutorial/welcome.png";
import tutorialChat from "./tutorial/chat.png";
import coins from "./tutorial/coins.png";
import followProgress from "./tutorial/followProgress.png";
import tutorialTasks from "./tutorial/tasks.png";
import loginAppDashboard from "./mockups/loginAppDashboard.png";
import writingBG from "./UI/writingBG.png";
import studioBG from "./UI/studioBG.png";
import studioBG2 from "./UI/studioBG2.png";
import productionBG from "./UI/productionBG.png";
import productionBG2 from "./UI/productionBG2.png";
import playbackBG from "./UI/playbackBG.png";
import playbackBG2 from "./UI/playbackBG2.png";
import libraryBG from "./UI/libraryBG.png";
import libraryBG2 from "./UI/libraryBG2.png";
import profileBG from "./UI/profileBG.png";
import hitParadeBG from "./UI/hitParadeBG.png";
import homeBG from "./UI/homeBG.png";
import nathalie from "./UI/nathalie.png";
import theo from "./UI/theo.png";
import bena from "./UI/bena.png";
import john from "./UI/john.png";
import placeholder from "./UI/placeholder.jpg";
import placeholder2 from "./UI/placeholder2.jpg";
import placeholder3 from "./UI/placeholder3.png";
import placeholder4 from "./UI/placeholder4.jpg";
import profile from "./UI/profile.jpg";
import goodVibe from "./UI/goodVibe.png";
export const tabs = {
home,
tasks,
settings,
quotes,
chat,
design,
addTab,
albums,
ribbon,
mic,
person,
};
export const icons = {
bell,
sort,
dragDots: require("./icons/dragDots.png"),
chevronDown,
arrowRight,
@@ -149,15 +192,6 @@ export const icons = {
coin,
};
import triangle from "./art/triangle.png";
import circle from "./art/circle.png";
import square from "./art/square.png";
import gradientTriangle from "./art/gradientTriangle.png";
import threeCoins from "./art/threeCoins.png";
import boost from "./art/boost.png";
import boostBig from "./art/boostBig.png";
export const art = {
triangle,
circle,
@@ -168,12 +202,6 @@ export const art = {
boostBig,
};
import welcome from "./tutorial/welcome.png";
import tutorialChat from "./tutorial/chat.png";
import coins from "./tutorial/coins.png";
import followProgress from "./tutorial/followProgress.png";
import tutorialTasks from "./tutorial/tasks.png";
export const tutorial = {
welcome,
chat: tutorialChat,
@@ -182,25 +210,10 @@ export const tutorial = {
tasks: tutorialTasks,
};
import loginAppDashboard from "./mockups/loginAppDashboard.png";
export const mockups = {
loginAppDashboard,
};
import writingBG from "./UI/writingBG.png";
import studioBG from "./UI/studioBG.png";
import studioBG2 from "./UI/studioBG2.png";
import productionBG from "./UI/productionBG.png";
import productionBG2 from "./UI/productionBG2.png";
import playbackBG from "./UI/playbackBG.png";
import playbackBG2 from "./UI/playbackBG2.png";
import libraryBG from "./UI/libraryBG.png";
import libraryBG2 from "./UI/libraryBG2.png";
import profileBG from "./UI/profileBG.png";
import hitParadeBG from "./UI/hitParadeBG.png";
import homeBG from "./UI/homeBG.png";
export const background = {
writingBG,
studioBG,
@@ -216,11 +229,6 @@ export const background = {
homeBG,
};
import nathalie from "./UI/nathalie.png";
import theo from "./UI/theo.png";
import bena from "./UI/bena.png";
import john from "./UI/john.png";
export const ai = {
nathalie,
theo,
@@ -228,13 +236,6 @@ export const ai = {
john,
};
import placeholder from "./UI/placeholder.jpg";
import placeholder2 from "./UI/placeholder2.jpg";
import placeholder3 from "./UI/placeholder3.png";
import placeholder4 from "./UI/placeholder4.jpg";
import profile from "./UI/profile.jpg";
import goodVibe from './UI/goodVibe.png'
export const img = {
placeholder,
placeholder2,
+1
View File
@@ -0,0 +1 @@
export class AppBlurView {}
+5 -1
View File
@@ -16,7 +16,11 @@ const BorderGradientButton = ({
disabled = false,
}) => {
return (
<Pressable onPress={onPress} disabled={disabled} style={{ ...containerStyle, opacity: disabled ? 0.6 : 1 }}>
<Pressable
onPress={onPress}
disabled={disabled}
style={{ ...containerStyle, opacity: disabled ? 0.6 : 1 }}
>
<BorderGradient
gradientProps={{
colors: ["#F94697", "#7023F7"],
+14 -3
View File
@@ -7,6 +7,7 @@ import Animated, { Easing, FadeIn, FadeOut } from "react-native-reanimated";
import { arrayRemove, playlistsRef } from "../config/firebase";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { Portal } from "@gorhom/portal";
const MoreMenu = ({
visible = false,
@@ -49,6 +50,7 @@ const MoreMenu = ({
};
return (
<Portal>
<View
style={{
position: "absolute",
@@ -60,9 +62,17 @@ const MoreMenu = ({
elevation: 9999,
}}
>
{/* Background overlay to close on outside press */}
<Pressable
style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}
onPress={onClose}
style={{
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex: 1,
}}
/>
<Animated.View
entering={FadeIn.duration(300).easing(Easing.ease)}
@@ -71,8 +81,8 @@ const MoreMenu = ({
position: "absolute",
right: 0,
top,
zIndex: 10000,
elevation: 10000,
zIndex: 2,
elevation: 2,
}}
>
<View style={{ gap: 2 }}>
@@ -170,6 +180,7 @@ const MoreMenu = ({
</View>
</Animated.View>
</View>
</Portal>
);
};
+7 -2
View File
@@ -1,4 +1,5 @@
import { Image, Text, View } from "react-native";
import { Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { responsiveWidth } from "../actions/responsiveSizes.js";
import { getInitials } from "../helpers";
import { Fonts, Style } from "../styles";
@@ -62,8 +63,12 @@ export default ({
}}
>
{url ? (
<Image
<ExpoImage
source={{ uri: url }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
width: size,
height: size,
+3 -7
View File
@@ -1,14 +1,10 @@
import { useContext } from "reactn";
import { Pressable, View, Image, Text } from "react-native";
import { Video, ResizeMode } from "expo-av";
import { Video, ResizeMode } from "expo-video";
import { Fonts, gutters, Palette } from "../styles";
import Style, { mainBorderRadius } from "../styles/Style";
import { icons } from "../assets";
import { substringTextLength } from "../helpers/index.js";
import { substringTextLength } from "../helpers";
import { WebViewContext } from "../providers/WebViewProvider.js";
const thumbnailStyle = {
@@ -20,7 +16,7 @@ const thumbnailStyle = {
const RenderChatFile = (
{ uri, type = "image", name = "", containerStyle = {} },
index
index,
) => {
const { setWebViewUrl } = useContext(WebViewContext);
+43 -6
View File
@@ -1,4 +1,4 @@
import React, { useRef, useState } from "react";
import React, { useMemo, useRef, useState } from "react";
import {
Dimensions,
FlatList,
@@ -33,6 +33,13 @@ const PlaylistModal = (props) => {
const startAtCreate = props?.payload?.startAtCreate || false;
const { setTooltip } = useMinuit();
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 (
<AppActionSheet id="Playlist">
<SwiperFlatList
@@ -106,9 +113,8 @@ const PlaylistModal = (props) => {
musics: arrayUnion(projectId),
updatedAt: new Date(),
});
const addedPlaylist = (Array.isArray(userPlaylists)
? userPlaylists
: []
const addedPlaylist = (
Array.isArray(userPlaylists) ? userPlaylists : []
).find((p) => p?.id === selectedId);
const name = addedPlaylist?.name || "la playlist";
setTooltip({
@@ -118,7 +124,10 @@ const PlaylistModal = (props) => {
}
} catch (e) {
console.log("Add to playlist error", e?.message);
setTooltip({ type: "error", text: e?.message || "Ajout impossible" });
setTooltip({
type: "error",
text: e?.message || "Ajout impossible",
});
} finally {
SheetManager.hide("Playlist");
}
@@ -160,6 +169,7 @@ const PlaylistModal = (props) => {
Nommer la playlist
</Text>
<TextInput
autoFocus={true}
style={{
paddingVertical: 6,
borderBottomWidth: 1,
@@ -174,6 +184,18 @@ const PlaylistModal = (props) => {
onChangeText={setPlaylist}
textAlign="center"
/>
{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"}
@@ -188,6 +210,18 @@ const PlaylistModal = (props) => {
try {
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);
if (exists) {
setTooltip({
type: "error",
text: "Ce nom de playlist existe déjà",
});
return;
}
await createPlaylist({
createdBy: currentUID,
name,
@@ -208,7 +242,10 @@ const PlaylistModal = (props) => {
scrollRef.current.scrollToIndex({ index: 0, animated: true });
} catch (e) {
console.log(e);
setTooltip({ type: "error", text: e?.message || "Création impossible" });
setTooltip({
type: "error",
text: e?.message || "Création impossible",
});
} finally {
setIsCreating(false);
}
-3
View File
@@ -49,9 +49,6 @@ try {
export const usersRef = firestore.collection("users");
export const projectsRef = firestore.collection("projects");
export const playlistsRef = firestore.collection("playlists");
export const followsRef = firestore.collection("follows");
export const notificationsRef = firestore.collection("notifications");
export const documentsRef = firestore.collection("documents");
export const chatsRef = firestore.collection("chats");
export const { arrayUnion, arrayRemove, increment, serverTimestamp } =
+10
View File
@@ -58,6 +58,8 @@ import ChangeEmailAddress from "../screens/Profile/ChangeEmailAddress";
import ChangePassword from "../screens/Profile/ChangePassword";
import Language from "../screens/Profile/Language";
import Reels from "../screens/Profile/Reels";
import Follows from "../screens/Profile/Follows";
import NewMusicOptions from "../screens/NewMusicOptions";
import Register from "../screens/Register";
import CreatePassword from "../screens/CreatePassword";
import CreatePseudo from "../screens/CreatePseudo";
@@ -255,6 +257,14 @@ const screens = [
name: Routes.Settings,
component: Settings,
},
{
name: Routes.Follows,
component: Follows,
},
{
name: Routes.FlowSelection,
component: NewMusicOptions,
},
{
name: Routes.ChangeEmailAddress,
component: ChangeEmailAddress,
+11
View File
@@ -1,4 +1,5 @@
import React, { setGlobal } from "reactn";
import { StackActions } from "@react-navigation/native";
import { isDesktop } from "../hooks/useLayoutType";
import { Routes } from "./Routes";
@@ -9,6 +10,16 @@ export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
export function push(name, params) {
if (!navigationRef.current) return;
try {
navigationRef.current.dispatch(StackActions.push(name, params));
} catch (e) {
// Fallback to navigate if push not available
navigationRef.current?.navigate?.(name, params);
}
}
export function dispatch(route) {
navigationRef.current?.dispatch(route);
}
+2
View File
@@ -79,4 +79,6 @@ export const Routes = {
Notifications: "Notifications",
Language: "Language",
Reels: "Reels",
Follows: "Follows",
FlowSelection: "FlowSelection",
};
+34 -27
View File
@@ -3,10 +3,11 @@ import { useDataFromRef } from "react-native-minuit/src/hooks";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import { createContext, useContext, useGlobal } from "reactn";
import { useState } from "react";
import { useEffect, useState } from "react";
import { checkIfEmailIsValid } from "../actions/signupActions";
import firebase, {
followsRef,
arrayRemove,
arrayUnion,
playlistsRef,
projectsRef,
usersRef,
@@ -24,26 +25,18 @@ export default ({ children }) => {
const [currentUID] = useGlobal("currentUID");
const [currentUserData, setCurrentUserData] = useGlobal("currentUserData");
const [currentUserRoles] = useGlobal("currentUserRoles");
const [userProjects, setUserProjects] = useState([]);
const [userLikedProjects, setUserLikedProjects] = useState([]);
const [userPlaylists, setUserPlaylists] = useState([]);
useDataFromRef({
// Current user document (live)
const { data: currentUserDoc = null } = useDataFromRef({
ref: currentUID ? usersRef.doc(currentUID) : null,
updateGlobalState: "currentUserData",
simpleRef: true,
listener: true,
condition: currentUID,
condition: !!currentUID,
refreshArray: [currentUID],
onUpdate: (data) => {
if (data) {
setCurrentUserData(data);
} else {
setCurrentUserData(null);
}
},
});
// Subscribe to user's projects (musics)
useDataFromRef({
const { data: userProjects = [] } = useDataFromRef({
ref: currentUID
? projectsRef
.where("userId", "==", currentUID)
@@ -53,11 +46,10 @@ export default ({ children }) => {
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
onUpdate: (list) => setUserProjects(Array.isArray(list) ? list : []),
});
// Subscribe to user's liked projects
useDataFromRef({
const { data: userLikedProjects = [] } = useDataFromRef({
ref: currentUID
? projectsRef
.where("likedBy", "array-contains", currentUID)
@@ -67,17 +59,26 @@ export default ({ children }) => {
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
onUpdate: (list) => setUserLikedProjects(Array.isArray(list) ? list : []),
});
// Subscribe to user's playlists
useDataFromRef({
const { data: userPlaylists = [] } = useDataFromRef({
ref: currentUID ? playlistsRef.where("createdBy", "==", currentUID) : null,
simpleRef: false,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
onUpdate: (list) => setUserPlaylists(Array.isArray(list) ? list : []),
});
// Live list of users that the current user is following ("abonnements")
const { data: userFollowing = [] } = useDataFromRef({
ref: currentUID
? usersRef.where("followedBy", "array-contains", currentUID)
: null,
simpleRef: false,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
});
const updatePendingUserData = (newData) => {
@@ -89,13 +90,12 @@ export default ({ children }) => {
const followUser = async (userId) => {
try {
if (currentUID && userId) {
await followsRef.doc(`${currentUID}_${userId}`).set(
await usersRef.doc(userId).set(
{
followerId: currentUID,
followeeId: userId,
createdAt: new Date(),
followedBy: arrayUnion(currentUID),
lastFollowersUpdateAt: new Date(),
},
{ merge: true }
{ merge: true },
);
setTooltip({ type: "success", text: "Abonnement mis à jour" });
}
@@ -108,7 +108,13 @@ export default ({ children }) => {
const unfollowUser = async (userId) => {
try {
if (currentUID && userId) {
await followsRef.doc(`${currentUID}_${userId}`).delete();
await usersRef.doc(userId).set(
{
followedBy: arrayRemove(currentUID),
lastFollowersUpdateAt: new Date(),
},
{ merge: true },
);
setTooltip({
type: "success",
text: "Vous ne suivez plus cet utilisateur",
@@ -234,10 +240,11 @@ export default ({ children }) => {
currentUserRoles,
currentUID,
currentUserData,
currentUserData: currentUserDoc,
userProjects,
userLikedProjects,
userPlaylists,
followingCount: userFollowing?.length || 0,
setCurrentUserData,
getUserByUid,
+15 -3
View File
@@ -1,4 +1,5 @@
import { View, Text, Image, Platform, Pressable } from "react-native";
import { View, Text, Platform, Pressable, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React from "react";
import { img } from "../../../assets";
import { BlurView } from "expo-blur";
@@ -16,10 +17,21 @@ const SongCard = ({ rank = 1, title = "Sans titre", artist = "MusicLand", coverU
gap: 6,
}}
>
<Image
source={coverUrl ? { uri: coverUrl } : img.placeholder2}
{coverUrl ? (
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
)}
<BlurView
intensity={20}
style={{
+105 -103
View File
@@ -1,53 +1,32 @@
import {
View,
Text,
Image,
ScrollView,
Pressable,
Platform,
} from "react-native";
import React from "react";
import { View, Text, Image, Platform } from "react-native";
import React, { useMemo, useState } from "react";
import Page from "../layouts/Page";
import { ai, background, img } from "../assets";
import { background, img } from "../assets";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { BlurView } from "expo-blur";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
const CREATE_DATA = [
{
img: ai.nathalie,
bg: background.writingBG,
label: "Nathalie",
desc: "Lets write lyrics together !",
type: "Songwriter",
},
{
img: ai.theo,
bg: background.studioBG,
label: "Theo",
desc: "Come back, when\nyoull have lyrics!",
type: "Beatmaker",
},
{
img: ai.bena,
bg: background.productionBG,
label: "Bena",
desc: "Come back when you want\nto publish the song!",
type: "Producer",
},
{
img: ai.john,
bg: background.playbackBG,
label: "John",
desc: "Come back when your\naudio is ready!",
type: "Director",
},
];
import { FlatList, Alert } from "react-native";
import { BlurView } from "expo-blur";
import { useUser } from "../providers/UserDataProvider";
import MusicCard from "./Library/components/MusicCard";
import GradientButton from "../components/GradientButton";
import MoreMenu from "../components/MoreMenu";
import { projectsRef } from "../config/firebase";
import { useGlobal } from "reactn";
const Home = () => {
const { userProjects = [] } = useUser();
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []),
[userProjects],
);
const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
return (
<Page backgroundImg={background.homeBG} headerType="NONE">
<View style={{ flex: 1 }}>
@@ -55,47 +34,22 @@ const Home = () => {
source={img.goodVibe}
style={{ alignSelf: "center", position: "absolute" }}
/>
<ScrollView
contentContainerStyle={{
flexGrow: 1,
gap: 57,
paddingBottom: responsiveHeight(20),
}}
showsVerticalScrollIndicator={false}
>
<View style={{ height: responsiveHeight(60) }} />
{CREATE_DATA.map((item, index) => (
<Pressable
key={index}
onPress={() => {
switch (index) {
case 0:
navigate(Routes.Create, {
action: item.type,
});
break;
case 1:
navigate(Routes.Studio, {
action: item.type,
});
break;
}
{projects.length > 0 && (
<View
style={{
height: responsiveHeight(70),
paddingTop: responsiveHeight(6),
}}
>
<BlurView
tint="dark"
intensity={20}
style={{
position: "absolute",
zIndex: 1,
left: 110,
top: 10,
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 14,
borderBottomLeftRadius: 0,
flex: 1,
borderRadius: 20,
overflow: "hidden",
backgroundColor: Palette.glass,
padding: 12,
gap: 8,
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
@@ -103,41 +57,89 @@ const Home = () => {
>
<Text
style={{
fontSize: 13,
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
marginBottom: 8,
textAlign: "center",
}}
>
{item.label}, {item.type}
Musiques en cours
</Text>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
<FlatList
data={projects}
keyExtractor={(item) => item.id}
contentContainerStyle={{ gap: 10, paddingBottom: 10 }}
renderItem={({ item }) => (
<MusicCard
title={item?.title || "Sans titre"}
subtitle={"MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() =>
navigate(Routes.FlowSelection, { projectId: item.id })
}
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
>
{item.desc}
</Text>
/>
)}
/>
<MoreMenu
visible={showMenu}
top={menuTop}
onClose={() => setShowMenu(false)}
inPlaylist={false}
projectId={selectedProjectId}
extraItems={[
{
label: "Supprimer",
onPress: () =>
Alert.alert(
"Confirmer la suppression",
"Cette action supprimera définitivement ce projet.",
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
if (!selectedProjectId) return;
await projectsRef
.doc(selectedProjectId)
.delete();
setTooltip({
type: "success",
text: "Projet supprimé",
});
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
}
},
},
],
{ cancelable: true },
),
},
]}
/>
</BlurView>
<Image
source={item.bg}
style={{
width: "100%",
height: 152,
borderRadius: 20,
position: "absolute",
bottom: 0,
}}
</View>
)}
<View style={{ paddingTop: 12, marginBottom: responsiveHeight(10) }}>
<GradientButton
title="Créer une nouvelle musique"
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => navigate(Routes.FlowSelection)}
/>
<Image
source={item.img}
style={{ width: 130, height: 200 }}
resizeMode="contain"
/>
</Pressable>
))}
</ScrollView>
</View>
</View>
</Page>
);
+15 -5
View File
@@ -1,4 +1,5 @@
import { View, Text, FlatList, Image, Pressable } from "react-native";
import { View, Text, FlatList, Pressable, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React, { useState } from "react";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
@@ -56,12 +57,21 @@ const AllMyMusic = () => {
navigate(Routes.MusicDetails, { projectId: item.id })
}
>
<Image
source={
item?.coverUrl ? { uri: item.coverUrl } : img.placeholder2
}
{item?.coverUrl ? (
<ExpoImage
source={{ uri: item.coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ width: "100%", height: 114, borderRadius: 8 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ width: "100%", height: 114, borderRadius: 8 }}
/>
)}
<Text
style={{
fontSize: 12,
+1
View File
@@ -23,6 +23,7 @@ const Library = () => {
<SearchBar
textInputProps={{
editable: false,
onPress: () => navigate(Routes.Research),
}}
/>
</Pressable>
+24 -22
View File
@@ -1,14 +1,8 @@
import { useRoute } from "@react-navigation/core";
import { Audio } from "expo-av";
import { Audio } from "expo-audio";
import React, { useEffect, useMemo, useState } from "react";
import {
Image,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { Pressable, ScrollView, StyleSheet, Text, View, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { SheetManager } from "react-native-actions-sheet";
import { useGlobal } from "reactn";
import { background, icons, img } from "../../assets";
@@ -78,7 +72,7 @@ const MusicDetails = () => {
const soundRef = React.useRef(null);
// Load/unload audio with expo-av for reliable status updates on iOS/Android
// Load/unload audio with expo-audio for reliable status updates on iOS/Android
useEffect(() => {
let isMounted = true;
const load = async () => {
@@ -95,16 +89,16 @@ const MusicDetails = () => {
if (!songUrl) return;
const { sound } = await Audio.Sound.createAsync(
{ uri: songUrl },
{ shouldPlay: false },
(status) => {
{ shouldPlay: false }
);
sound.setOnPlaybackStatusUpdate((status) => {
if (!isMounted) return;
if (!status || !status.isLoaded) return;
const pos = status.positionMillis || 0;
const dur = status.durationMillis || 0;
setProgressInfo({ pos, dur });
setIsPlaying(!!status.isPlaying);
}
);
});
soundRef.current = sound;
} catch (e) {
console.log("Audio load error", e?.message);
@@ -236,7 +230,7 @@ const MusicDetails = () => {
},
rightComponent: () => (
<Pressable onPress={() => SheetManager.show("DeleteAudio")}>
<Image source={icons.trash} />
<RNImage source={icons.trash} />
</Pressable>
),
})}
@@ -250,10 +244,18 @@ const MusicDetails = () => {
// stickyHeaderIndices={[1]}
>
<View style={{ gap: 28 }}>
<Image
source={coverUrl ? { uri: coverUrl } : img.placeholder4}
{coverUrl ? (
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={150}
style={styles.img}
/>
) : (
<RNImage source={img.placeholder4} style={styles.img} />
)}
<View style={{ ...Style.containerSpaceBetween }}>
<View>
<Text style={styles.title}>{title}</Text>
@@ -276,7 +278,7 @@ const MusicDetails = () => {
}
}}
>
<Image
<RNImage
source={fav ? icons.heart : icons.heartOutline}
style={{ ...size({ size: 24 }) }}
resizeMode="contain"
@@ -287,7 +289,7 @@ const MusicDetails = () => {
SheetManager.show("Playlist", { payload: { projectId } })
}
>
<Image
<RNImage
source={icons.more}
style={{ ...size({ size: 24 }) }}
resizeMode="contain"
@@ -335,7 +337,7 @@ const MusicDetails = () => {
<View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}>
{/* Previous (rewind 10s) */}
<Pressable onPress={() => seekBy(-10)}>
<Image
<RNImage
source={icons.forward}
style={{
...size({ size: 30 }),
@@ -351,7 +353,7 @@ const MusicDetails = () => {
}}
onPress={togglePlay}
>
<Image
<RNImage
resizeMode={"contain"}
source={isPlaying ? icons.pause : icons.play}
style={size({ size: 34 })}
@@ -359,7 +361,7 @@ const MusicDetails = () => {
</Pressable>
{/* Next (forward 10s) */}
<Pressable onPress={() => seekBy(10)}>
<Image
<RNImage
source={icons.forward}
style={{
...size({ size: 30 }),
+37 -3
View File
@@ -1,7 +1,8 @@
import React, { useState } from "react";
import { View } from "react-native";
import { View, Pressable, Image, Alert } from "react-native";
import { useGlobal } from "reactn";
import Page from "../../layouts/Page";
import { background } from "../../assets";
import { background, icons } from "../../assets";
import { gutters } from "../../styles";
import { useRoute } from "@react-navigation/native";
import { useDataFromRef } from "react-native-minuit/src/hooks";
@@ -9,10 +10,11 @@ import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId
import { playlistsRef, projectsRef } from "../../config/firebase";
import MusicCard from "./components/MusicCard";
import MoreMenu from "../../components/MoreMenu";
import { navigate } from "../../navigation/NavigationService";
import { navigate, goBack } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const PlaylistDetails = () => {
const [, setTooltip] = useGlobal("_tooltip");
const route = useRoute();
const playlistId = route?.params?.playlistId;
@@ -37,11 +39,43 @@ const PlaylistDetails = () => {
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const confirmDelete = () => {
Alert.alert(
"Supprimer la playlist",
"Es-tu sûr de vouloir supprimer cette playlist ?",
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer",
style: "destructive",
onPress: async () => {
try {
if (playlistId) {
await playlistsRef.doc(playlistId).delete();
setTooltip({ type: "success", text: "Playlist supprimée" });
}
} catch (e) {
console.log("Delete playlist error", e?.message);
setTooltip({ type: "error", text: e?.message || "Suppression impossible" });
} finally {
goBack();
}
},
},
],
);
};
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.libraryBG}
title={playlist?.name || "Playlist"}
rightComponent={() => (
<Pressable onPress={confirmDelete}>
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
</Pressable>
)}
contentContainerStyle={{ paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, gap: 10 }}>
+178 -67
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from "react";
import { Image, Pressable, ScrollView, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { background, img } from "../../assets";
import MoreMenu from "../../components/MoreMenu";
import { usersRef, projectsRef } from "../../config/firebase";
@@ -19,32 +20,53 @@ const Research = () => {
const [selected, setSelected] = useState(null);
const [search, setSearch] = useState("");
const queryText = useMemo(() => search.trim(), [search]);
const queryLower = useMemo(
() => (search || "").toLowerCase().trim(),
[search],
);
const [currentUID] = useGlobal("currentUID");
const [autoLoads, setAutoLoads] = useState(0);
const usersQueryRef = useMemo(() => {
try {
if (queryText) {
const lower = queryLower;
if (lower) {
return usersRef
.orderBy("userName")
.startAt(queryText)
.endAt(queryText + "\uf8ff");
.orderBy("userNameLower")
.startAt(lower)
.endAt(lower + "\uf8ff");
}
return usersRef.orderBy("userName");
return usersRef.orderBy("userNameLower");
} catch (e) {
return null;
}
}, [queryLower]);
const projectsQueryRefLower = useMemo(() => {
try {
const lower = queryText ? queryText.toLowerCase() : "";
if (lower) {
return projectsRef
.orderBy("titleLower")
.startAt(lower)
.endAt(lower + "\uf8ff");
}
return null;
} catch (e) {
return null;
}
}, [queryText]);
const projectsQueryRef = useMemo(() => {
const projectsQueryRefCase = useMemo(() => {
try {
if (queryText) {
const raw = queryText || "";
if (raw) {
return projectsRef
.orderBy("title")
.startAt(queryText)
.endAt(queryText + "\uf8ff");
.startAt(raw)
.endAt(raw + "\uf8ff");
}
return projectsRef.orderBy("updatedAt", "desc");
return null;
} catch (e) {
return null;
}
@@ -59,12 +81,19 @@ const Research = () => {
simpleRef: false,
listener: false,
condition: !!usersQueryRef,
refreshArray: [queryText],
refreshArray: [queryLower],
usePagination: true,
batchSize: 5,
});
const filteredUsers = useMemo(() => {
const arr = Array.isArray(userResults) ? userResults : [];
let arr = Array.isArray(userResults) ? userResults : [];
// Client-side safety filter to guarantee match on pseudo
if (queryLower) {
arr = arr.filter((u) => {
const nameLower = (u?.userNameLower || u?.userName || "").toLowerCase();
return nameLower.includes(queryLower);
});
}
const seen = new Set();
const unique = [];
for (const u of arr) {
@@ -75,24 +104,83 @@ const Research = () => {
unique.push(u);
}
return unique;
}, [JSON.stringify(userResults), currentUID]);
}, [JSON.stringify(userResults), queryLower, currentUID]);
const {
data: projectResults = [],
loading: projectsLoading,
loadMore: loadMoreProjects,
data: projectResultsLower = [],
loading: projectsLoadingLower,
loadMore: loadMoreProjectsLower,
} = useDataFromRef({
ref: projectsQueryRef,
ref: projectsQueryRefLower,
simpleRef: false,
listener: false,
condition: !!projectsQueryRef,
condition: !!projectsQueryRefLower,
refreshArray: [queryText],
usePagination: true,
batchSize: 6,
});
const {
data: projectResultsCase = [],
loading: projectsLoadingCase,
loadMore: loadMoreProjectsCase,
} = useDataFromRef({
ref: projectsQueryRefCase,
simpleRef: false,
listener: false,
condition: !!projectsQueryRefCase,
refreshArray: [queryText],
usePagination: true,
batchSize: 6,
});
const {
data: projectResultsDefault = [],
loading: projectsLoadingDefault,
loadMore: loadMoreProjectsDefault,
} = useDataFromRef({
ref: !queryText ? projectsRef.orderBy("updatedAt", "desc") : null,
simpleRef: false,
listener: false,
condition: !queryText,
refreshArray: [queryText],
usePagination: true,
batchSize: 6,
});
const projectsLoading = queryText
? projectsLoadingLower || projectsLoadingCase
: projectsLoadingDefault;
const loadMoreProjects = () => {
if (queryText) {
loadMoreProjectsLower?.();
loadMoreProjectsCase?.();
} else {
loadMoreProjectsDefault?.();
}
};
const filteredProjects = useMemo(() => {
return Array.isArray(projectResults) ? projectResults : [];
}, [JSON.stringify(projectResults)]);
const arr = queryText
? [
...(Array.isArray(projectResultsLower) ? projectResultsLower : []),
...(Array.isArray(projectResultsCase) ? projectResultsCase : []),
]
: Array.isArray(projectResultsDefault)
? projectResultsDefault
: [];
const seen = new Set();
const unique = [];
for (const p of arr) {
const id = p?.id;
if (!id || seen.has(id)) continue;
seen.add(id);
unique.push(p);
}
return unique;
}, [
queryText,
JSON.stringify(projectResultsLower),
JSON.stringify(projectResultsCase),
JSON.stringify(projectResultsDefault),
]);
useEffect(() => {
setAutoLoads(0);
@@ -105,13 +193,7 @@ const Research = () => {
loadMoreUsers();
}
}
}, [
filteredUsers.length,
usersLoading,
usersQueryRef,
autoLoads,
loadMoreUsers,
]);
}, [filteredUsers.length, usersLoading, usersQueryRef, autoLoads]);
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
@@ -124,6 +206,21 @@ const Research = () => {
}
};
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
return (
<Page
headerType="NAVIGATION"
@@ -142,7 +239,15 @@ const Research = () => {
/>
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingTop: 2 }}>
{(!selected || selected === "Musiques") && (
{(!selected &&
!projectsLoading &&
!usersLoading &&
filteredProjects.length === 0 &&
filteredUsers.length === 0) && (
<EmptyText text={"Aucun résultat"} />
)}
{((!selected && (filteredProjects.length > 0 || projectsLoading)) ||
selected === "Musiques") && (
<View style={{ paddingHorizontal: 2 }}>
<CreateLyricsHeader
gradientProps={{
@@ -150,6 +255,7 @@ const Research = () => {
end: { x: 0, y: 1 },
}}
>
{filteredProjects.length > 0 ? (
<View style={{ gap: 10 }}>
{Array.isArray(filteredProjects) &&
filteredProjects.slice(0, 6).map((p) => (
@@ -166,7 +272,9 @@ const Research = () => {
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
setShowMenu(
(prev) => !prev || posTop !== menuTop,
);
}}
/>
))}
@@ -188,48 +296,21 @@ const Research = () => {
</Pressable>
)}
</View>
) : (
!projectsLoading && (
<EmptyText text={"Aucune musique trouvée"} />
)
)}
</CreateLyricsHeader>
</View>
)}
{(!selected || selected === "Clips") && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
<View style={{ gap: 6, ...Style.containerRow }}>
{Array.from({ length: 3 }).map((_, index) => (
<View style={{ flex: 1, gap: 6 }} key={index}>
<Image
source={img.placeholder3}
style={{
width: "100%",
height: 147,
borderRadius: 10,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Pour Lili
</Text>
</View>
))}
</View>
</CreateLyricsHeader>
</View>
)}
{(!selected || selected === "Profils") && (
{((!selected && (filteredUsers.length > 0 || usersLoading)) ||
selected === "Profils") && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
{filteredUsers.length > 0 ? (
<View style={{ gap: 8 }}>
{Array.isArray(filteredUsers) &&
filteredUsers.slice(0, 5).map((u) => (
@@ -240,9 +321,11 @@ const Research = () => {
navigate(Routes.SingerProfile, { userId: u.id })
}
>
<Image
<ExpoImage
source={
u?.pictureUrl || u?.profilePictureURL || u?.photoURL
u?.pictureUrl ||
u?.profilePictureURL ||
u?.photoURL
? {
uri:
u?.pictureUrl ||
@@ -251,7 +334,14 @@ const Research = () => {
}
: img.profile
}
style={{ ...size({ size: 60 }), borderRadius: 100 }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 60 }),
borderRadius: 100,
}}
/>
<Text
style={{
@@ -282,6 +372,27 @@ const Research = () => {
</Pressable>
)}
</View>
) : (
!usersLoading && <EmptyText text={"Aucun profil trouvé"} />
)}
</CreateLyricsHeader>
</View>
)}
{selected === "Playbacks" && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<EmptyText text={"Aucun playback trouvé"} />
</CreateLyricsHeader>
</View>
)}
{selected === "Clips" && (
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
<CreateLyricsHeader
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
>
<EmptyText text={"Aucun clip trouvé"} />
</CreateLyricsHeader>
</View>
)}
+18 -5
View File
@@ -1,5 +1,6 @@
import React from "react";
import { FlatList, Image, Pressable, useWindowDimensions } from "react-native";
import { FlatList, Pressable, useWindowDimensions, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { img } from "../../../assets";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
@@ -36,15 +37,27 @@ const LikedMusic = () => {
navigate(Routes.MusicDetails, { projectId: item.id })
}
>
<Image
source={
item?.coverUrl ? { uri: item.coverUrl } : img.placeholder3
}
{item?.coverUrl ? (
<ExpoImage
source={{ uri: item.coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
height: itemWidth - containerGap,
borderRadius: 10,
}}
/>
) : (
<RNImage
source={img.placeholder3}
style={{
height: itemWidth - containerGap,
borderRadius: 10,
}}
/>
)}
</Pressable>
)}
/>
+33 -13
View File
@@ -1,13 +1,7 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useState } from "react";
import {
Image,
Platform,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useRef, useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { useGlobal } from "reactn";
import { icons, img } from "../../../assets";
import { arrayRemove, arrayUnion, projectsRef } from "../../../config/firebase";
@@ -28,6 +22,7 @@ const MusicCard = ({
const [selected, setSelected] = useState(false);
const [layout, setLayout] = useState(null);
const [menuPos, setMenuPos] = useState(0);
const cardRef = useRef(null);
useEffect(() => {
if (layout) {
@@ -57,6 +52,19 @@ const MusicCard = ({
};
const onPressMenu = () => {
// Prefer absolute screen position when available (Portal rendering)
if (cardRef?.current?.measureInWindow) {
try {
cardRef.current.measureInWindow((x, y, width, height) => {
const top = (y || 0) + 45;
setMenuPos(top);
onPressMore?.(top);
});
return;
} catch (e) {
// fallback to relative layout position
}
}
onPressMore?.(menuPos);
};
@@ -67,13 +75,25 @@ const MusicCard = ({
...Style.containerRow,
gap: 6,
}}
ref={cardRef}
onPress={onPress}
onLayout={(e) => setLayout(e.nativeEvent.layout)}
>
<Image
source={imageUri ? { uri: imageUri } : img.placeholder2}
{imageUri ? (
<ExpoImage
source={{ uri: imageUri }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
) : (
<RNImage
source={img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }}
/>
)}
<View style={styles.blurContainer}>
<BlurView
intensity={20}
@@ -97,14 +117,14 @@ const MusicCard = ({
}}
>
<Pressable onPress={toggleLike}>
<Image
<RNImage
source={selected ? icons.heart : icons.heartOutline}
style={size({ size: 24 })}
resizeMode="contain"
/>
</Pressable>
<Pressable onPress={onPressMenu}>
<Image source={icons.more} style={size({ size: 24 })} />
<RNImage source={icons.more} style={size({ size: 24 })} />
</Pressable>
</View>
</BlurView>
@@ -6,13 +6,19 @@ import BorderGradient from "../../../components/BorderGradient/BorderGradient";
import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../../../styles/Fonts";
const ResearchHeader = ({ onPress, selected, searchValue = "", onChangeSearch = () => {} }) => {
const ResearchHeader = ({
onPress,
selected,
searchValue = "",
onChangeSearch = () => {},
}) => {
return (
<View style={{ gap: 12 }}>
<SearchBar
textInputProps={{
value: searchValue,
onChangeText: onChangeSearch,
autoFocus: true,
}}
/>
<View
+194
View File
@@ -0,0 +1,194 @@
import React, { useMemo } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import Page from "../layouts/Page";
import { ai, background } from "../assets";
import { BlurView } from "expo-blur";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import palette from "../styles/Palette";
const CREATE_DATA = [
{
img: ai.nathalie,
bg: background.writingBG,
label: "Nathalie",
desc: "Lets write lyrics together !",
type: "Songwriter",
},
{
img: ai.theo,
bg: background.studioBG,
label: "Theo",
desc: "Come back, when\nyoull have lyrics!",
type: "Beatmaker",
},
{
img: ai.bena,
bg: background.productionBG,
label: "Bena",
desc: "Come back when you want\nto publish the song!",
type: "Producer",
},
{
img: ai.john,
bg: background.playbackBG,
label: "John",
desc: "Come back when your\naudio is ready!",
type: "Director",
},
];
const NewMusicOptions = ({ route }) => {
const { userProjects } = useUserData();
const { projectId = null } = route?.params || {};
const currentProjet = useMemo(() => {
if (!projectId) {
return null;
}
return userProjects?.find((p) => p.id === projectId) || null;
}, [projectId, userProjects]);
const hasProject = !!currentProjet;
const hasLyrics = Array.isArray(currentProjet?.lyrics)
? currentProjet.lyrics.length > 0
: !!currentProjet?.lyrics;
const hasCover = !!currentProjet?.coverUrl;
const isLocked = (index) => {
// 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director
if (index === 3) return true; // Director toujours verrouillé
if (!hasProject) return index !== 0; // seulement Songwriter
if (hasCover) return index !== 2; // seulement Producer
if (hasLyrics) return !(index === 0 || index === 1); // Songwriter + Beatmaker
return index !== 0; // par défaut seulement Songwriter
};
const onPressOption = (index, item) => {
if (isLocked(index)) return;
switch (index) {
case 0:
navigate(Routes.WritingLyrics, {});
break;
case 1:
navigate(Routes.Studio, { action: item.type });
break;
case 2:
navigate(Routes.Production, { action: item.type });
break;
default:
break;
}
};
return (
<Page
backgroundImg={background.homeBG}
headerType="NAVIGATION"
title={!!currentProjet ? "Continuer la création" : "Nouvelle musique"}
scrollEnabled={true}
>
<View style={{ gap: 10 }}>
{CREATE_DATA.map((item, index) => {
const locked = isLocked(index);
return (
<Pressable key={index} onPress={() => onPressOption(index, item)}>
<BlurView
tint="dark"
intensity={20}
style={{
position: "absolute",
zIndex: 1,
left: 110,
top: 10,
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 14,
borderBottomLeftRadius: 0,
overflow: "hidden",
backgroundColor: Palette.glass,
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<Text
style={{
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{item.label}, {item.type}
</Text>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item.desc}
</Text>
</BlurView>
<Image
source={item.bg}
style={{
width: "100%",
height: 152,
borderRadius: 20,
position: "absolute",
bottom: 0,
}}
/>
<Image
source={item.img}
style={{ width: 130, height: 200 }}
resizeMode="contain"
/>
{locked && (
<View
pointerEvents="none"
style={{
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.35)",
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.4)",
alignItems: "center",
justifyContent: "center",
}}
>
<FontAwesome
name="lock"
size={24}
color={palette.primary}
/>
</View>
</View>
)}
</Pressable>
);
})}
</View>
</Page>
);
};
export default NewMusicOptions;
+7 -2
View File
@@ -1,7 +1,8 @@
import { BlurView } from "expo-blur";
import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useState } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { Platform, Pressable, Text, View, Image as RNImage } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, img } from "../../assets";
@@ -145,7 +146,7 @@ const EditProfile = () => {
}
>
<Pressable style={{ alignItems: "center" }} onPress={onChangePicture}>
<Image
<ExpoImage
source={
currentUserData?.profilePictureURL
? {
@@ -153,6 +154,10 @@ const EditProfile = () => {
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 108 }),
borderRadius: 100,
+174
View File
@@ -0,0 +1,174 @@
import React, { useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import Page from "../../layouts/Page";
import { background, img } from "../../assets";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import { useUser } from "../../providers/UserDataProvider";
import { usersRef } from "../../config/firebase";
import useDataFromArrayId from "react-native-minuit/src/hooks/useDataFromArrayId";
import useDataFromRef from "../../hooks/useDataFromRef";
import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { navigate } from "../../navigation/NavigationService";
const Follows = () => {
const { params } = useRoute();
const initial = params?.selected || "Abonnés"; // "Abonnés" | "Abonnements"
const targetUserId = params?.userId || null;
const [selected, setSelected] = useState(initial);
const { currentUID, currentUserData } = useUser();
// Abonnés (followers): docs for IDs in currentUserData.followedBy
// If viewing another user's profile, subscribe to that user's doc
const { data: targetUserDoc = null } = useDataFromRef({
ref: targetUserId ? usersRef.doc(targetUserId) : null,
simpleRef: true,
listener: true,
condition: !!targetUserId,
refreshArray: [targetUserId],
});
const followerIds = useMemo(() => {
if (targetUserId) {
return Array.isArray(targetUserDoc?.followedBy)
? targetUserDoc.followedBy
: [];
}
return Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy
: [];
}, [
targetUserId,
targetUserDoc?.followedBy?.length || 0,
currentUserData?.followedBy?.length || 0,
]);
const { data: followers = [], loading: followersLoading } =
useDataFromArrayId({
ref: usersRef,
arrayId: followerIds,
condition: followerIds.length > 0,
refreshArray: [followerIds.length],
});
// Abonnements (following): users whose followedBy includes currentUID
const { data: following = [], loading: followingLoading } = useDataFromRef({
ref:
targetUserId || currentUID
? usersRef.where(
"followedBy",
"array-contains",
targetUserId || currentUID,
)
: null,
simpleRef: false,
listener: true,
condition: !!(targetUserId || currentUID),
refreshArray: [targetUserId || currentUID],
});
const EmptyText = ({ text }) => (
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
const UserRow = ({ user }) => (
<Pressable
style={{ gap: 14, ...Style.containerRow }}
onPress={() => navigate(Routes.SingerProfile, { userId: user?.id })}
>
<ExpoImage
source={
user?.profilePictureURL
? { uri: user.profilePictureURL }
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{ ...size({ size: 60 }), borderRadius: 100 }}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{user?.userName || "Utilisateur"}
</Text>
</Pressable>
);
const list = selected === "Abonnés" ? followers : following;
const loading = selected === "Abonnés" ? followersLoading : followingLoading;
return (
<Page
headerType="NAVIGATION"
backgroundImg={background.profileBG}
title={"Abonnés et abonnements"}
contentContainerStyle={{ paddingBottom: 20 }}
>
<View style={{ gap: 12 }}>
<View style={{ ...Style.containerRow, gap: 6 }}>
{["Abonnés", "Abonnements"].map((item) => (
<Pressable
key={item}
onPress={() => setSelected(item)}
style={{ flex: 1 }}
>
<View
style={{
height: 30,
borderRadius: 100,
overflow: "hidden",
backgroundColor:
selected === item ? "#FFFFFF22" : Palette.glass,
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{item}
</Text>
</View>
</Pressable>
))}
</View>
<View style={{ gap: 8 }}>
{Array.isArray(list) && list.length > 0 ? (
list.map((u) => <UserRow key={u.id} user={u} />)
) : !loading ? (
<EmptyText
text={
selected === "Abonnés" ? "Aucun abonné" : "Aucun abonnement"
}
/>
) : null}
</View>
</View>
</Page>
);
};
export default Follows;
+57 -55
View File
@@ -1,5 +1,4 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
FlatList,
Image,
@@ -10,6 +9,7 @@ import {
Text,
View,
} from "react-native";
import { Image as ExpoImage } from "expo-image";
import { SheetManager } from "react-native-actions-sheet";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
@@ -20,31 +20,40 @@ import MoreMenu from "../../components/MoreMenu";
import { projectsRef } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { navigate, push } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { getFollowersCount, getFollowingCount } from "../../services/follows";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { gutters, size } from "../../styles/Style";
import MusicCard from "../Library/components/MusicCard";
import { BlurView } from "expo-blur";
const Profile = () => {
const [selected, setSelected] = useState("Chansons");
const [currentUserData] = useGlobal("currentUserData");
const { userProjects, currentUID } = useUser();
const { userProjects, followingCount, currentUserData } = useUser();
const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const [following, setFollowing] = useState(0);
const [followers, setFollowers] = useState(0);
const onPressMenu = (item) => {
setSelected(item);
};
useEffect(() => {
if (!currentUID) return;
getFollowersCount(currentUID).then(setFollowers);
getFollowingCount(currentUID).then(setFollowing);
}, [currentUID]);
const EmptyText = ({ text }) => (
<View style={{ flex: 1, ...Style.containerCenter, paddingTop: 20 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
return (
<Page
backgroundImg={background.profileBG}
@@ -76,7 +85,7 @@ const Profile = () => {
}
>
<View style={{ alignItems: "center" }}>
<Image
<ExpoImage
source={
currentUserData?.profilePictureURL
? {
@@ -84,6 +93,10 @@ const Profile = () => {
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 108 }),
borderRadius: 100,
@@ -101,19 +114,27 @@ const Profile = () => {
</View>
<View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>{userProjects?.length || 0}</Text>
<Text style={styles.label}>projets</Text>
</View>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() => push(Routes.Follows, { selected: "Abonnés" })}
>
<Text style={styles.value}>
{currentUserData?.playlist?.length || 0}
{currentUserData?.followedBy?.length || 0}
</Text>
<Text style={styles.label}>playbacks</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>{followers}</Text>
<Text style={styles.label}>abonnés</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>{following}</Text>
</Pressable>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() =>
push(Routes.Follows, { selected: "Abonnements" })
}
>
<Text style={styles.value}>{followingCount}</Text>
<Text style={styles.label}>abonnements</Text>
</View>
</Pressable>
</View>
</BlurView>
</View>
@@ -157,42 +178,14 @@ const Profile = () => {
</View>
{selected === "Playbacks" && (
<View style={{ flex: 1 }}>
<FlatList
data={Array.from({ length: 6 })}
numColumns={3}
contentContainerStyle={{ gap: 14 }}
columnWrapperStyle={{ gap: 10 }}
renderItem={() => (
<Pressable
style={{ flex: 1, gap: 6 }}
onPress={() => navigate(Routes.Reels)}
>
<Image
source={img.placeholder3}
style={{
width: "100%",
height: 147,
borderRadius: 10,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Pour Lili
</Text>
</Pressable>
)}
/>
<EmptyText text={"Aucun playback pour le moment"} />
</View>
)}
{selected === "Chansons" && (
<ScrollView style={{ flex: 1 }}>
{Array.isArray(userProjects) && userProjects.length > 0 ? (
<FlatList
data={Array.isArray(userProjects) ? userProjects : []}
data={userProjects}
contentContainerStyle={{
gap: 10,
}}
@@ -215,6 +208,9 @@ const Profile = () => {
/>
)}
/>
) : (
<EmptyText text={"Aucune chanson pour le moment"} />
)}
<MoreMenu
visible={showMenu}
top={menuTop}
@@ -249,13 +245,19 @@ const Profile = () => {
},
},
],
{ cancelable: true }
{ cancelable: true },
),
},
]}
/>
</ScrollView>
)}
{selected === "Clips" && (
<View style={{ flex: 1 }}>
<EmptyText text={"Aucun clip pour le moment"} />
</View>
)}
</View>
</Page>
);
+74 -52
View File
@@ -1,4 +1,3 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useState } from "react";
import {
FlatList,
@@ -9,23 +8,26 @@ import {
Text,
View,
} from "react-native";
import { Image as ExpoImage } from "expo-image";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
import { background, icons, img } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
import { followsRef, projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import { projectsRef } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { navigate, push } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { getFollowersCount, getFollowingCount } from "../../services/follows";
import useDataFromRef from "../../hooks/useDataFromRef";
import { usersRef } from "../../config/firebase";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { size } from "../../styles/Style";
import MusicCard from "../Library/components/MusicCard";
import { BlurView } from "expo-blur";
const SingerProfile = ({ route }) => {
const { userId } = route.params;
const { getUserByUid, followUser, unfollowUser } = useUser();
@@ -37,25 +39,32 @@ const SingerProfile = ({ route }) => {
const fetchUserData = async () => {
const data = userId ? await getUserByUid(userId) : {};
setUserData(data);
const list = Array.isArray(data?.followedBy) ? data.followedBy : [];
setFollowers(list.length);
setIsFollowing(currentUID ? list.includes(currentUID) : false);
};
useEffect(() => {
fetchUserData();
}, [userId]);
useEffect(() => {
if (!userId) return;
getFollowersCount(userId).then(setFollowers);
getFollowingCount(userId).then(setFollowing);
(async () => {
// Live following count for the viewed user
const followingQueryRef = React.useMemo(() => {
try {
if (currentUID && userId) {
const doc = await followsRef.doc(`${currentUID}_${userId}`).get();
setIsFollowing(!!doc?.exists);
} else {
setIsFollowing(false);
return userId
? usersRef.where("followedBy", "array-contains", userId)
: null;
} catch (e) {
return null;
}
} catch (_) {}
})();
}, [userId, currentUID]);
}, [userId]);
useDataFromRef({
ref: followingQueryRef,
simpleRef: false,
listener: true,
condition: !!followingQueryRef,
refreshArray: [userId],
onUpdate: (list) => setFollowing(Array.isArray(list) ? list.length : 0),
});
const onPressMenu = (item) => {
if (selected === item) {
setSelected(null);
@@ -85,6 +94,21 @@ const SingerProfile = ({ route }) => {
condition: !!userProjectsQueryRef,
refreshArray: [userId],
});
const EmptyText = ({ text }) => (
<View style={{ flex: 1, ...Style.containerCenter, paddingTop: 20 }}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8,
}}
>
{text}
</Text>
</View>
);
const handleFollowUser = async () => {
if (!currentUID || !userId || currentUID === userId) return;
if (isFollowing) {
@@ -127,7 +151,7 @@ const SingerProfile = ({ route }) => {
}
>
<View style={{ alignItems: "center" }}>
<Image
<ExpoImage
source={
userData?.profilePictureURL
? {
@@ -135,6 +159,10 @@ const SingerProfile = ({ route }) => {
}
: img.profile
}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={100}
style={{
...size({ size: 108 }),
borderRadius: 100,
@@ -155,16 +183,26 @@ const SingerProfile = ({ route }) => {
<Text style={styles.value}>
{Array.isArray(userProjects) ? userProjects.length : 0}
</Text>
<Text style={styles.label}>playbacks</Text>
<Text style={styles.label}>projets</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() =>
push(Routes.Follows, { selected: "Abonnés", userId })
}
>
<Text style={styles.value}>{followers}</Text>
<Text style={styles.label}>abonnés</Text>
</View>
<View style={{ flex: 1, alignItems: "center" }}>
</Pressable>
<Pressable
style={{ flex: 1, alignItems: "center" }}
onPress={() =>
push(Routes.Follows, { selected: "Abonnements", userId })
}
>
<Text style={styles.value}>{following}</Text>
<Text style={styles.label}>abonnements</Text>
</View>
</Pressable>
</View>
{currentUID !== userId && (
<GradientButton
@@ -219,39 +257,14 @@ const SingerProfile = ({ route }) => {
</View>
{selected === "Playbacks" && (
<View style={{ flex: 1 }}>
<FlatList
data={Array.from({ length: 6 })}
numColumns={3}
contentContainerStyle={{ gap: 14 }}
columnWrapperStyle={{ gap: 10 }}
renderItem={() => (
<View style={{ flex: 1, gap: 6 }}>
<Image
source={img.placeholder3}
style={{
width: "100%",
height: 147,
borderRadius: 10,
}}
/>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Pour Lili
</Text>
</View>
)}
/>
<EmptyText text={"Aucun playback pour le moment"} />
</View>
)}
{selected === "Chansons" && (
<View style={{ flex: 1 }}>
{Array.isArray(userProjects) && userProjects.length > 0 ? (
<FlatList
data={Array.isArray(userProjects) ? userProjects : []}
data={userProjects}
contentContainerStyle={{
gap: 10,
}}
@@ -274,6 +287,15 @@ const SingerProfile = ({ route }) => {
/>
)}
/>
) : (
<EmptyText text={"Aucune chanson pour le moment"} />
)}
</View>
)}
{selected === "Clips" && (
<View style={{ flex: 1 }}>
<EmptyText text={"Aucun clip pour le moment"} />
</View>
)}
<MoreMenu
+6 -1
View File
@@ -1,4 +1,5 @@
import { View, Text, Image, ActivityIndicator } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page";
import { background, icons, img } from "../../assets";
@@ -83,8 +84,12 @@ const PouchReady = () => {
<View style={{ flex: 1, ...Style.containerCenter }}>
<View style={{ width: "80%", position: "relative" }}>
{coverUrl ? (
<Image
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={120}
style={{ width: "100%", height: 300, borderRadius: 20 }}
/>
) : (
-2
View File
@@ -6,8 +6,6 @@ import GradientButton from "../../components/GradientButton";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import { gutters } from "../../styles";
import firebase from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { useUser } from "../../providers/UserDataProvider";
import { responsiveHeight } from "react-native-responsive-dimensions";
+60 -43
View File
@@ -1,13 +1,13 @@
import { View, Dimensions } from "react-native";
import React, { useMemo, useRef, useState } from "react";
import { useRoute } from "@react-navigation/native";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton";
import { gutters } from "../../styles";
import { SwiperFlatList } from "react-native-swiper-flatlist";
import Goals from "./Goals";
import { goBack } from "../../navigation/NavigationService";
import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import SpecificityContext from "./SpecificityContext";
import EmotionConvey from "./EmotionConvey";
import SongStyle from "./SongStyle";
@@ -20,42 +20,24 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
const { width } = Dimensions.get("window");
const CreateLyricsWithAi = () => {
const CreateLyricsWithAi = ({ route }) => {
const { hasLyrics = false, regenerateKey } = route?.params || {};
const scrollRef = useRef(null);
const route = useRoute();
const regenerateKey = route?.params?.regenerateKey;
const [selectedIndex, setSelectedIndex] = useState(0);
// If user already has lyrics, start at SongStructure (index 5)
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
// Collected state across steps
const [objective, setObjective] = useState(
__DEV__ ? "Pour mon entreprise" : null,
); // from Goals list
const [objective, setObjective] = useState(null); // from Goals list
const [otherObjective, setOtherObjective] = useState("");
const [context, setContext] = useState(
__DEV__
? "L'agence minuit est une agence de développement mobile et web qui accompagnes ses client dans la réalisations de projets divers et variés"
: "",
);
const [emotion, setEmotion] = useState(
__DEV__
? {
title: "La Joie",
description:
"expose un bonheur profond, l'émerveillement, la gratitude, satisfaction intense, énergie positive",
}
: null,
); // { title, description }
const [style, setStyle] = useState(__DEV__ ? "Upbeat" : null); // from list
const [context, setContext] = useState("");
const [emotion, setEmotion] = useState(null);
const [style, setStyle] = useState(null); // from list
const [otherStyle, setOtherStyle] = useState("");
const [audience, setAudience] = useState(
__DEV__ ? "Aux clients de l'agence minuit" : "",
);
const [structure, setStructure] = useState(
__DEV__ ? "1 couplet, 1 refrain, 1 couplet, 1 refrain" : null,
); // selected structure string
const [rhymes, setRhymes] = useState(__DEV__ ? "Avec rimes" : null);
const [audience, setAudience] = useState("");
const [structure, setStructure] = useState(null); // selected structure string
const [rhymes, setRhymes] = useState(null);
const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain']
const parsedStructure = useMemo(() => {
@@ -117,27 +99,61 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = () => {
setSelectedIndex(selectedIndex + 1);
setProgress(progress + 9);
scrollRef.current.scrollToIndex({
index: selectedIndex + 1,
animated: true,
// If user already has lyrics and just finished CustomizeSongStructure (index 6),
// skip AI generation and go straight to Lyrics editor
if (hasLyrics && selectedIndex === 6) {
const chosenStructure =
(customStructure &&
parsedStructure &&
customStructure.length === parsedStructure.length
? customStructure
: parsedStructure) || [];
navigate(Routes.Lyrics, {
config: { structure: chosenStructure },
selections: {
objective,
otherObjective,
context,
emotion,
style,
otherStyle,
audience,
structure,
parsedStructure,
customStructure,
rhymes,
},
hasLyrics: true,
});
return;
}
setSelectedIndex((idx) => idx + 1);
};
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
setProgress(progress - 9);
scrollRef.current.scrollToIndex({
index: selectedIndex - 1,
animated: true,
});
// If currently on SongStructure, go back to previous screen instead of previous step
if (selectedIndex === 5) {
goBack();
} else if (selectedIndex > 0) {
setSelectedIndex((idx) => Math.max(0, idx - 1));
} else {
goBack();
}
};
// Sync scroll position and progress with selectedIndex
React.useEffect(() => {
if (!parentLayout?.height) return;
try {
const nextProgress = 16 + selectedIndex * 9;
setProgress(nextProgress);
scrollRef.current?.scrollToIndex?.({
index: selectedIndex,
animated: true,
});
} catch (_) {}
}, [selectedIndex, parentLayout?.height]);
return (
<Page headerType="NONE">
<MusicLandHeader
@@ -160,6 +176,7 @@ const CreateLyricsWithAi = () => {
style={{ width, left: -gutters }}
ref={scrollRef}
disableGesture
index={selectedIndex}
>
<View
style={{
+1
View File
@@ -168,6 +168,7 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
lyricsData: result,
config,
selections,
hasLyrics: false,
})
}
/>
+85 -70
View File
@@ -1,84 +1,93 @@
import { View, Text, StyleSheet, ScrollView } from "react-native";
import React, { useMemo, useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { View, StyleSheet, Image, Platform, Text } from "react-native";
import React, { useMemo, useState, useCallback, useEffect } from "react";
import { BlurView } from "expo-blur";
import { Palette } from "../../styles";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { CUSTOM_SONG_STRUCTURE } from "../../data/data";
import SongStructureDragDrop from "../../components/SongStructureDragDrop";
import Sortable from "react-native-sortables";
import { icons } from "../../assets";
// baseStructure: array like ['couplet','refrain',...]
// onChange: callback that receives array like ['couplet','refrain',...]
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const [containerLayout, setContainerLayout] = useState(null);
const sourceItems = useMemo(() => {
// create unique ids for duplicates
// Build stable keyed data like { 'couplet-1': { label, value }, ... }
const data = useMemo(() => {
const counters = { couplet: 0, refrain: 0 };
return (baseStructure || []).map((type) => {
const key = (type || '').toLowerCase();
const out = {};
(baseStructure || []).forEach((type) => {
const key = (type || "").toLowerCase();
counters[key] = (counters[key] || 0) + 1;
const idx = counters[key];
return {
id: `${key}-${idx}`,
label: `${type} ${idx}`,
const id = `${key}-${idx}`;
out[id] = {
id,
label: `${key === "couplet" ? "Couplet" : "Refrain"} ${idx}`,
value: key,
};
});
return out;
}, [baseStructure]);
const items = useMemo(() => Object.values(data), [data]);
// Initialize parent with current order (so it's saved even without drag)
useEffect(() => {
const initialValues = (items || []).map((it) => it?.value).filter(Boolean);
onChange?.(initialValues);
// We only want to run when baseStructure-derived items change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items.length]);
const Row = ({ item: rowData }) => (
<View style={styles.itemContainer}>
<BlurView
intensity={30}
style={styles.rowBlur}
experimentalBlurMethod={Platform.OS !== "ios" ? "dimezisBlurView" : "none"}
>
<View style={{ ...Style.containerSpaceBetween, alignItems: "center" }}>
<View>
<View>
<View>
<View />
</View>
</View>
<View>
<View />
</View>
</View>
<View style={{ flex: 1, justifyContent: "center" }}>
<View>
<Text style={styles.rowText}>{rowData?.label || ""}</Text>
</View>
</View>
<Sortable.Handle>
<Image source={icons.dragDots} style={styles.handleIcon} />
</Sortable.Handle>
</View>
</BlurView>
</View>
);
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title="Personnalise la structure de ta chanson"
subTitle="Intègre en Drag and drop"
subTitle="Réorganise par glisser-déposer"
/>
{/* <View style={{ flex: 1, gap: 10 }}>
<View
style={{ flex: 1 }}
onLayout={(e) => {
setContainerLayout(e.nativeEvent.layout);
<View style={{ flex: 1 }} onLayout={(e) => setContainerLayout(e.nativeEvent.layout)}>
<Sortable.Grid
columns={1}
rowGap={12}
data={items}
renderItem={({ item }) => <Row item={item} />}
customHandle
onDragEnd={({ data: newData }) => {
const values = (newData || []).map((it) => it?.value).filter(Boolean);
onChange?.(values);
}}
>
<ItemContainer height={containerLayout?.height}>
<ScrollView contentContainerStyle={{ gap: 10, paddingBottom: 20 }}>
{["Couplet", "Refrain", "Couplet", "Refrain"].map(
(item, index) => (
<View key={index} style={styles.dropzoneContainer}>
<Text style={styles.dropzone}>{item}</Text>
</View>
)
)}
</ScrollView>
</ItemContainer>
</View>
<View style={{ flex: 1 }}>
<ItemContainer height={containerLayout?.height}>
<ScrollView contentContainerStyle={{ gap: 10, paddingBottom: 20 }}>
{CUSTOM_SONG_STRUCTURE.map((item, index) => (
<View key={index} style={styles.itemContainer}>
<BlurView
intensity={30}
style={{
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
}}
>
<Text style={styles.dropzone}>{item}</Text>
</BlurView>
</View>
))}
</ScrollView>
</ItemContainer>
</View>
</View> */}
<View style={{ flex: 1 }}>
<SongStructureDragDrop
sourceItems={sourceItems}
onChange={(arr) => onChange?.(arr)}
/>
</View>
</View>
@@ -88,17 +97,6 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
export default CustomizeSongStructure;
const styles = StyleSheet.create({
dropzoneContainer: {
paddingVertical: 14,
paddingHorizontal: 12,
backgroundColor: "#00000080",
borderRadius: 14,
},
dropzone: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
itemContainer: {
height: 54,
backgroundColor: Palette.glass,
@@ -113,4 +111,21 @@ const styles = StyleSheet.create({
shadowRadius: 3.84,
elevation: 5,
},
rowBlur: {
flex: 1,
backgroundColor: Palette.glass,
justifyContent: "center",
paddingHorizontal: 12,
height: "100%",
},
rowText: {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
handleIcon: {
width: 22,
height: 22,
tintColor: Palette.white,
},
});
+6 -4
View File
@@ -1,13 +1,15 @@
import { View, Text, FlatList } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { EMOTION_CONVEY } from "../../data/data";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const EmotionConvey = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
const EmotionConvey = ({
selected: selectedProp,
setSelected: setSelectedProp,
}) => {
const [containerLayout, setContainerLayout] = useState(null);
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
@@ -25,8 +27,8 @@ const EmotionConvey = ({ selected: selectedProp, setSelected: setSelectedProp })
return (
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader
title={`Quel émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne tes intentions émotionnelles. (2 max)"
title={`Quelle émotion veux-tu\ntransmettre ?`}
subTitle="Sélectionne une seule intention émotionnelle."
/>
<View
+40 -11
View File
@@ -1,17 +1,16 @@
import { View, Text, ScrollView, Alert } from "react-native";
import { View, ScrollView, Alert } from "react-native";
import React, { useMemo, useState, useCallback } from "react";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { gutters } from "../../styles";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import CustomInput from "./components/CustomInput";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { useRoute } from "@react-navigation/native";
import firebase from "../../config/firebase";
import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
const Lyrics = ({ navigation }) => {
@@ -20,17 +19,30 @@ const Lyrics = ({ navigation }) => {
const lyricsData = route?.params?.lyricsData;
const config = route?.params?.config;
const selections = route?.params?.selections;
const hasLyrics = !!route?.params?.hasLyrics;
const { setIsLoading } = useMinuit();
const initial = useMemo(() => {
const title = lyricsData?.title || "";
const aiSections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
const aiSections = Array.isArray(lyricsData?.lyrics)
? lyricsData.lyrics
: [];
// Respecter l'ordre de la structure choisie si disponible
const targetStructure = Array.isArray(config?.structure)
? config.structure.map((t) => (t || "").toLowerCase())
: null;
if (aiSections.length && targetStructure && aiSections.length === targetStructure.length) {
return { title, sections: aiSections.map((s) => ({ type: (s?.type || "").toLowerCase(), lyrics: s?.lyrics || "" })) };
if (
aiSections.length &&
targetStructure &&
aiSections.length === targetStructure.length
) {
return {
title,
sections: aiSections.map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
})),
};
}
// Sinon, créer à partir de la structure
if (targetStructure && targetStructure.length) {
@@ -77,9 +89,23 @@ const Lyrics = ({ navigation }) => {
const onValidate = useCallback(async () => {
try {
await setIsLoading(true);
const titleTrimmed = (titleValue || "").trim();
if (!titleTrimmed) {
Alert.alert("Titre manquant", "Veuillez renseigner un titre.");
return;
}
const invalid = (sections || []).some((s) => !(s?.lyrics || "").trim());
if (invalid) {
Alert.alert(
"Champs incomplets",
"Chaque couplet et refrain doit contenir du texte.",
);
return;
}
const user = firebase.auth().currentUser;
const payload = {
title: titleValue?.trim() || "",
title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(),
lyrics: (sections || []).map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
@@ -89,9 +115,10 @@ const Lyrics = ({ navigation }) => {
userId: user ? user.uid : null,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
hasLyrics,
};
await firebase.firestore().collection("projects").add(payload);
navigate(Routes.Studio);
const { id: projectId } = await projectsRef.add(payload);
navigate(Routes.FlowSelection, { projectId });
} catch (e) {
console.log(e);
Alert.alert("Erreur", "Échec de l'enregistrement dans le projet.");
@@ -157,10 +184,12 @@ const Lyrics = ({ navigation }) => {
gap: 12,
}}
>
{!hasLyrics && (
<BorderGradientButton
title="Générer d'autre paroles"
title="Générer d'autres paroles"
onPress={regenerate}
/>
)}
<GradientButton title="Valider" onPress={onValidate} />
</View>
</Page>
+7 -5
View File
@@ -1,13 +1,15 @@
import { View, Text, StyleSheet, FlatList, Pressable } from "react-native";
import { View, Text, StyleSheet, FlatList } from "react-native";
import React, { useState } from "react";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
import { BlurView } from "expo-blur";
import { GOALS, SONG_STRUCTURE } from "../../data/data";
import { Palette, Style } from "../../styles";
import { SONG_STRUCTURE } from "../../data/data";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const SongStructure = ({ selected: selectedProp, setSelected: setSelectedProp }) => {
const SongStructure = ({
selected: selectedProp,
setSelected: setSelectedProp,
}) => {
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected;
+6 -1
View File
@@ -8,7 +8,12 @@ import CustomInput from "./components/CustomInput";
import { FONT_FAMILY } from "../../styles/Fonts";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
const SongStyle = ({ selected: selectedProp, setSelected: setSelectedProp, otherStyle, setOtherStyle }) => {
const SongStyle = ({
selected: selectedProp,
setSelected: setSelectedProp,
otherStyle,
setOtherStyle,
}) => {
const [internalSelected, setInternalSelected] = useState(null);
const selected = selectedProp ?? internalSelected;
const setSelected = setSelectedProp ?? setInternalSelected;
+8 -2
View File
@@ -13,7 +13,7 @@ const WritingLyrics = () => {
return (
<Page headerType="NONE">
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
<MusicLandHeader showSkip onPressBack={goBack} progress={9} />
<MusicLandHeader onPressBack={goBack} progress={9} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
@@ -24,7 +24,13 @@ const WritingLyrics = () => {
gap: 12,
}}
>
<BorderGradientButton />
<BorderGradientButton
onPress={() => {
navigate(Routes.CreateLyricsWithAi, {
hasLyrics: true,
});
}}
/>
<GradientButton
title="Écrire des paroles avec une IA"
onPress={() => navigate(Routes.CreateLyricsWithAi)}
-20
View File
@@ -1,20 +0,0 @@
import { followsRef } from "../config/firebase";
export const getFollowersCount = async (userId) => {
if (!userId) return 0;
const snap = await followsRef.where("followeeId", "==", userId).get();
return snap?.size || 0;
};
export const getFollowingCount = async (userId) => {
if (!userId) return 0;
const snap = await followsRef.where("followerId", "==", userId).get();
return snap?.size || 0;
};
export const unfollowUser = async ({ followerId, followeeId }) => {
if (!followerId || !followeeId) throw new Error("Erreur lors du unfollow");
const id = `${followerId}_${followeeId}`;
await followsRef.doc(id).delete();
return id;
};
+108 -96
View File
@@ -981,24 +981,24 @@
"@types/hammerjs" "^2.0.36"
"@emnapi/core@^1.4.3":
version "1.4.5"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb"
integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==
version "1.5.0"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0"
integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==
dependencies:
"@emnapi/wasi-threads" "1.0.4"
"@emnapi/wasi-threads" "1.1.0"
tslib "^2.4.0"
"@emnapi/runtime@^1.4.3":
version "1.4.5"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9"
integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==
version "1.5.0"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73"
integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4"
integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==
"@emnapi/wasi-threads@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
dependencies:
tslib "^2.4.0"
@@ -2608,78 +2608,78 @@
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^8.9.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz#19f959f273b32f5082c891903645e6a85328db4e"
integrity sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz#42209e2ce3e2274de0f5f9b75c777deedacaa558"
integrity sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.40.0"
"@typescript-eslint/type-utils" "8.40.0"
"@typescript-eslint/utils" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/scope-manager" "8.41.0"
"@typescript-eslint/type-utils" "8.41.0"
"@typescript-eslint/utils" "8.41.0"
"@typescript-eslint/visitor-keys" "8.41.0"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^8.9.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.40.0.tgz#1bc9f3701ced29540eb76ff2d95ce0d52ddc7e69"
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.41.0.tgz#677f5b2b3fa947ee1eac4129220c051b1990d898"
integrity sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==
dependencies:
"@typescript-eslint/scope-manager" "8.40.0"
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/scope-manager" "8.41.0"
"@typescript-eslint/types" "8.41.0"
"@typescript-eslint/typescript-estree" "8.41.0"
"@typescript-eslint/visitor-keys" "8.41.0"
debug "^4.3.4"
"@typescript-eslint/project-service@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz#1b7ba6079ff580c3215882fe75a43e5d3ed166b9"
integrity sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==
"@typescript-eslint/project-service@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.41.0.tgz#08ebf882d413a038926e73fda36e00c3dba84882"
integrity sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.40.0"
"@typescript-eslint/types" "^8.40.0"
"@typescript-eslint/tsconfig-utils" "^8.41.0"
"@typescript-eslint/types" "^8.41.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz#2fbfcc8643340d8cd692267e61548b946190be8a"
integrity sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==
"@typescript-eslint/scope-manager@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz#c8aba12129cb9cead1f1727f58e6a0fcebeecdb5"
integrity sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==
dependencies:
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/types" "8.41.0"
"@typescript-eslint/visitor-keys" "8.41.0"
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz#8e8fdb9b988854aedd04abdde3239c4bdd2d26e4"
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
"@typescript-eslint/tsconfig-utils@8.41.0", "@typescript-eslint/tsconfig-utils@^8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz#134dee36eb16cdd78095a20bca0516d10b5dda75"
integrity sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==
"@typescript-eslint/type-utils@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz#a7e4a1f0815dd0ba3e4eef945cc87193ca32c422"
integrity sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==
"@typescript-eslint/type-utils@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz#68d401e38fccf239925447e97bdbd048a9891ae5"
integrity sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==
dependencies:
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/utils" "8.40.0"
"@typescript-eslint/types" "8.41.0"
"@typescript-eslint/typescript-estree" "8.41.0"
"@typescript-eslint/utils" "8.41.0"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.29.1", "@typescript-eslint/types@^8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.40.0.tgz#0b580fdf643737aa5c01285314b5c6e9543846a9"
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
"@typescript-eslint/types@8.41.0", "@typescript-eslint/types@^8.29.1", "@typescript-eslint/types@^8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.41.0.tgz#9935afeaae65e535abcbcee95383fa649c64d16d"
integrity sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==
"@typescript-eslint/typescript-estree@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz#295149440ce7da81c790a4e14e327599a3a1e5c9"
integrity sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==
"@typescript-eslint/typescript-estree@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz#7c9cff8b4334ce96f14e9689692e8cf426ce4d59"
integrity sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==
dependencies:
"@typescript-eslint/project-service" "8.40.0"
"@typescript-eslint/tsconfig-utils" "8.40.0"
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/visitor-keys" "8.40.0"
"@typescript-eslint/project-service" "8.41.0"
"@typescript-eslint/tsconfig-utils" "8.41.0"
"@typescript-eslint/types" "8.41.0"
"@typescript-eslint/visitor-keys" "8.41.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -2687,22 +2687,22 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.40.0", "@typescript-eslint/utils@^8.29.1":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.40.0.tgz#8d0c6430ed2f5dc350784bb0d8be514da1e54054"
integrity sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==
"@typescript-eslint/utils@8.41.0", "@typescript-eslint/utils@^8.29.1":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.41.0.tgz#17cb3b766c1626311004ea41ffd8c27eb226b953"
integrity sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.40.0"
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/typescript-estree" "8.40.0"
"@typescript-eslint/scope-manager" "8.41.0"
"@typescript-eslint/types" "8.41.0"
"@typescript-eslint/typescript-estree" "8.41.0"
"@typescript-eslint/visitor-keys@8.40.0":
version "8.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz#c1b45196981311fed7256863be4bfb2d3eda332a"
integrity sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==
"@typescript-eslint/visitor-keys@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz#16eb99b55d207f6688002a2cf425e039579aa9a9"
integrity sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==
dependencies:
"@typescript-eslint/types" "8.40.0"
"@typescript-eslint/types" "8.41.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.2.0":
@@ -3437,12 +3437,12 @@ braces@^3.0.3:
fill-range "^7.1.1"
browserslist@^4.24.0, browserslist@^4.25.3:
version "4.25.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5"
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
version "4.25.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.4.tgz#ebdd0e1d1cf3911834bab3a6cd7b917d9babf5af"
integrity sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==
dependencies:
caniuse-lite "^1.0.30001735"
electron-to-chromium "^1.5.204"
caniuse-lite "^1.0.30001737"
electron-to-chromium "^1.5.211"
node-releases "^2.0.19"
update-browserslist-db "^1.1.3"
@@ -3603,10 +3603,10 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001735:
version "1.0.30001737"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc"
integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==
caniuse-lite@^1.0.30001737:
version "1.0.30001739"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4"
integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==
caw@^2.0.0, caw@^2.0.1:
version "2.0.1"
@@ -4386,10 +4386,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.204:
version "1.5.208"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz#609c29502fd7257b4d721e3446f3ae391a0ca1b3"
integrity sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==
electron-to-chromium@^1.5.211:
version "1.5.211"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz#749317bf9cf894c06f67980940cf8074e5eb08ca"
integrity sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==
emoji-regex@^8.0.0:
version "8.0.0"
@@ -5007,11 +5007,6 @@ expo-audio@~0.3.5:
resolved "https://registry.yarnpkg.com/expo-audio/-/expo-audio-0.3.5.tgz#1f151ec9919e163019f1aa76a117657e0ea6b613"
integrity sha512-gzpDH3vZI1FDL1Q8pXryACtNIW+idZ/zIZ8WqdTRzJuzxucazrG2gLXUS2ngcXQBn09Jyz4RUnU10Tu2N7/Hgg==
expo-av@~15.0.2:
version "15.0.2"
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-15.0.2.tgz#65bb08658a7fe3a67aa47da614abbfae9adb684e"
integrity sha512-AHIHXdqLgK1dfHZF0JzX3YSVySGMrWn9QtPzaVjw54FAzvXfMt4sIoq4qRL/9XWCP9+ICcCs/u3EcvmxQjrfcA==
expo-blur@~14.0.3:
version "14.0.3"
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-14.0.3.tgz#656d6b2442bfbbfb2a6608c6bc1151b29bce6698"
@@ -5220,6 +5215,11 @@ expo-updates-interface@~1.0.0:
resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz#b98c66b800d29561c62409556948b2af3d5316e5"
integrity sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ==
expo-video@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-2.0.6.tgz#4da4e4d427ad42649881a5336697b5a7164aaab3"
integrity sha512-oDorKwjreHT596oY6ZfktbDa3VF+Sq7gVsTbnQ4QVqGYL13mIO0s+h4y9xMyG/cDWyFpiRc4UT8KkPdxL0wViQ==
expo@~52.0.47:
version "52.0.47"
resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.47.tgz#b2a25bdcbd2059094396750e896946d2e2e61753"
@@ -5313,9 +5313,9 @@ fast-loops@^1.1.3:
integrity sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==
fast-uri@^3.0.1:
version "3.0.6"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748"
integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==
version "3.1.0"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
fast-xml-parser@^4.4.1:
version "4.5.3"
@@ -5600,9 +5600,9 @@ flow-enums-runtime@^0.0.6:
integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==
flow-parser@0.*:
version "0.279.0"
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.279.0.tgz#30cfb614b2a5157a3c40640c1c5ee90f888fade7"
integrity sha512-41VremrzImoLcZuqY18U86ojcVy2Stuq4VnjdAcxHjGanvx3VmKVUITIVMt2PM1RvmRJtgtJWvCxVpQ1E9OGDw==
version "0.280.0"
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.280.0.tgz#d19efc98528556597c2d184cf14b4e597c108f67"
integrity sha512-BblDQXb41t9gwFHJNR8EhLdS/9fqK/mCBZLRHiUccA2V1VoYIKuutglO/TAuJfUU3tolQlvMaW1S/KbRDR0rNQ==
fontfaceobserver@^2.1.0:
version "2.3.0"
@@ -8704,6 +8704,11 @@ react-native-gesture-handler@~2.20.2:
invariant "^2.2.4"
prop-types "^15.7.2"
react-native-haptic-feedback@>=2.0.0:
version "2.3.3"
resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.3.tgz#88b6876e91399a69bd1b551fe1681b2f3dc1214e"
integrity sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ==
react-native-hyperlink@^0.0.22:
version "0.0.22"
resolved "https://registry.yarnpkg.com/react-native-hyperlink/-/react-native-hyperlink-0.0.22.tgz#9f553390b5f96ae473d942639c40bce6579abc89"
@@ -8797,6 +8802,13 @@ react-native-safe-area-context@4.5.0:
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz#9208313236e8f49e1920ac1e2a2c975f03aed284"
integrity sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ==
react-native-sortables@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/react-native-sortables/-/react-native-sortables-1.8.0.tgz#fabdc8a757eb1e847eda3b4af1f9d349780a1079"
integrity sha512-uoNY0uiHikq3eKJoF85CATMPpo9jS7t37XUlunxSGjjbGi4L05os8eJBGNZSR+3NQgK2FtYGGkQtFbeSJIwmDg==
optionalDependencies:
react-native-haptic-feedback ">=2.0.0"
react-native-store-review@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/react-native-store-review/-/react-native-store-review-0.3.0.tgz#67480b9736c10bd00ad8d4d80324d2146e18d7dd"