Change main page and modify flows
This commit is contained in:
@@ -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
@@ -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({
|
||||
|
||||
Generated
+1976
-62
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export class AppBlurView {}
|
||||
@@ -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"],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 } =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -79,4 +79,6 @@ export const Routes = {
|
||||
Notifications: "Notifications",
|
||||
Language: "Language",
|
||||
Reels: "Reels",
|
||||
Follows: "Follows",
|
||||
FlowSelection: "FlowSelection",
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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: "Let’s write lyrics together !",
|
||||
type: "Songwriter",
|
||||
},
|
||||
{
|
||||
img: ai.theo,
|
||||
bg: background.studioBG,
|
||||
label: "Theo",
|
||||
desc: "Come back, when\nyou’ll 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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,6 +23,7 @@ const Library = () => {
|
||||
<SearchBar
|
||||
textInputProps={{
|
||||
editable: false,
|
||||
onPress: () => navigate(Routes.Research),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "Let’s write lyrics together !",
|
||||
type: "Songwriter",
|
||||
},
|
||||
{
|
||||
img: ai.theo,
|
||||
bg: background.studioBG,
|
||||
label: "Theo",
|
||||
desc: "Come back, when\nyou’ll 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -168,6 +168,7 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
|
||||
lyricsData: result,
|
||||
config,
|
||||
selections,
|
||||
hasLyrics: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user