continue generating flow, add backend connection on main tabs

This commit is contained in:
Thomas Demirdjian
2025-08-28 17:18:28 +02:00
parent 0ca7544005
commit bb7b35626f
110 changed files with 1701 additions and 3408 deletions
+3 -1
View File
@@ -9,6 +9,8 @@ npm-debug.*
*.mobileprovision *.mobileprovision
*.orig.* *.orig.*
web-build/ web-build/
ios/
android/
# macOS # macOS
.DS_Store .DS_Store
@@ -73,4 +75,4 @@ buck-out/
web-build/ web-build/
dist/ dist/
# @end expo-cli # @end expo-cli
+2 -3
View File
@@ -92,7 +92,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "2024.04.1" versionName "0.0.1"
} }
signingConfigs { signingConfigs {
debug { debug {
@@ -175,5 +175,4 @@ dependencies {
} }
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
+1 -1
View File
@@ -31,7 +31,7 @@
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="minuit"/> <data android:scheme="minuit"/>
<data android:scheme="com.minuit.starter"/> <data android:scheme="com.minuit.starter"/>
<data android:scheme="exp+minuitstarter"/> <data android:scheme="exp+musicland"/>
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

+3 -3
View File
@@ -1,7 +1,7 @@
<resources> <resources>
<color name="splashscreen_background">#0F0C14</color> <color name="splashscreen_background">#0A0D12</color>
<color name="iconBackground">#FFFFFF</color> <color name="iconBackground">#0A0D12</color>
<color name="colorPrimary">#023c69</color> <color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#0F0C14</color> <color name="colorPrimaryDark">#0A0D12</color>
<color name="activityBackground">#0F0C14</color> <color name="activityBackground">#0F0C14</color>
</resources> </resources>
+2 -2
View File
@@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">minuit.starter</string> <string name="app_name">MusicLand</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string> <string name="expo_splash_screen_resize_mode" translatable="false">cover</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string> <string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources> </resources>
+1 -1
View File
@@ -4,7 +4,7 @@
<item name="android:editTextStyle">@style/ResetEditText</item> <item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#0F0C14</item> <item name="android:statusBarColor">#0A0D12</item>
<item name="android:windowBackground">@color/activityBackground</item> <item name="android:windowBackground">@color/activityBackground</item>
</style> </style>
<style name="ResetEditText" parent="@android:style/Widget.EditText"> <style name="ResetEditText" parent="@android:style/Widget.EditText">
-1
View File
@@ -15,7 +15,6 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3'
classpath 'com.google.gms:google-services:4.4.1' classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle') classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin') classpath('com.facebook.react:react-native-gradle-plugin')
+1 -1
View File
@@ -58,4 +58,4 @@ expo.useLegacyPackaging=false
android.compileSdkVersion=35 android.compileSdkVersion=35
android.targetSdkVersion=35 android.targetSdkVersion=35
android.buildToolsVersion=35.0.0 android.buildToolsVersion=35.0.0
android.extraMavenRepos=[{"url":"../../node_modules/@notifee/react-native/android/libs"}] android.extraMavenRepos=[]
+1 -1
View File
@@ -21,7 +21,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
} }
} }
rootProject.name = 'minuit.starter' rootProject.name = 'MusicLand'
dependencyResolutionManagement { dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
+7 -13
View File
@@ -1,12 +1,12 @@
{ {
"expo": { "expo": {
"owner": "minuitagency", "owner": "minuitagency",
"slug": "minuitstarter", "slug": "musicland",
"name": "minuit.starter", "name": "MusicLand",
"version": "2024.04.1", "version": "0.0.1",
"scheme": "minuit", "scheme": "minuit",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/alternateIcons/4.png", "icon": "./assets/icon.png",
"backgroundColor": "#0F0C14", "backgroundColor": "#0F0C14",
"jsEngine": "hermes", "jsEngine": "hermes",
"notification": { "notification": {
@@ -27,9 +27,8 @@
}, },
"splash": { "splash": {
"image": "./assets/splash.png", "image": "./assets/splash.png",
"tabletImage": "./assets/tabletSplash.png", "resizeMode": "cover",
"resizeMode": "contain", "backgroundColor": "#0A0D12"
"backgroundColor": "#0F0C14"
}, },
"ios": { "ios": {
"googleServicesFile": "./config/GoogleService-Info.plist", "googleServicesFile": "./config/GoogleService-Info.plist",
@@ -66,7 +65,7 @@
"googleServicesFile": "./config/google-services.json", "googleServicesFile": "./config/google-services.json",
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png", "foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF" "backgroundColor": "#0A0D12"
}, },
"package": "com.minuit.starter", "package": "com.minuit.starter",
"permissions": [ "permissions": [
@@ -78,8 +77,6 @@
}, },
"plugins": [ "plugins": [
"react-native-compressor", "react-native-compressor",
"@react-native-firebase/crashlytics",
"@react-native-firebase/app",
[ [
"expo-image-picker", "expo-image-picker",
{ {
@@ -96,9 +93,6 @@
"useFrameworks": "static" "useFrameworks": "static"
}, },
"android": { "android": {
"extraMavenRepos": [
"../../node_modules/@notifee/react-native/android/libs"
],
"compileSdkVersion": 35, "compileSdkVersion": 35,
"targetSdkVersion": 35, "targetSdkVersion": 35,
"buildToolsVersion": "35.0.0" "buildToolsVersion": "35.0.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 240 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+100 -3
View File
@@ -1,8 +1,9 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const serviceAccount = require("./serviceAccountKey.json"); const serviceAccount = require("./serviceAccountKey.json");
const { googleAI, gemini25FlashLite } = require("@genkit-ai/googleai"); const { googleAI } = require("@genkit-ai/googleai");
const { GEMINI_API_KEY } = require("./config/keys"); const { GEMINI_API_KEY } = require("./config/keys");
const { genkit } = require("genkit"); const { genkit, z } = require("genkit");
const axios = require("axios");
admin.initializeApp({ admin.initializeApp({
credential: admin.credential.cert(serviceAccount), credential: admin.credential.cert(serviceAccount),
@@ -16,7 +17,7 @@ exports.generateAI = async ({ system = "", prompt = "", schema }) => {
apiKey: GEMINI_API_KEY, apiKey: GEMINI_API_KEY,
}), }),
], ],
model: gemini25FlashLite, model: googleAI.model("gemini-2.5-flash"),
}); });
if (prompt?.length < 1) { if (prompt?.length < 1) {
@@ -34,6 +35,102 @@ exports.generateAI = async ({ system = "", prompt = "", schema }) => {
return output; return output;
}; };
// Générateur d'images via appel direct REST Imagen 3.0 (retour base64 pur)
exports.generateImage = async ({ system = "", prompt = "" }) => {
if (typeof prompt !== "string" || prompt.trim().length < 1) {
throw new Error(
"Vous devez spécifier un prompt pour effectuer cette action.",
);
}
const baseUrl =
"https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict";
const url = `${baseUrl}?key=${encodeURIComponent(GEMINI_API_KEY)}`;
const mkBody = (p) => ({
instances: [
{
prompt: p,
},
],
parameters: {
sampleCount: 1,
aspectRatio: "1:1",
personGeneration: "dont_allow",
},
});
const combinedPrompt = [
system,
"Contraintes supplémentaires: pas de personnes, pas de visages, style abstrait, formes géométriques, textures et motifs.",
prompt,
]
.filter(Boolean)
.join("\n\n");
const tryOnce = async (p) => {
const resp = await axios.post(url, mkBody(p), {
headers: { "Content-Type": "application/json" },
timeout: 120000,
});
const predictions = resp?.data?.predictions || [];
const first = predictions.find((x) => x?.bytesBase64Encoded);
return first
? {
b64: first.bytesBase64Encoded,
mimeType: first.mimeType || "image/png",
}
: { b64: "", mimeType: null };
};
// Primary try
let result = { b64: "", mimeType: null };
try {
result = await tryOnce(combinedPrompt);
} catch (e) {
const em = e?.response?.data || e?.message || e;
console.error("Imagen predict error", em);
}
// Fallbacks on safety filters/no predictions
if (!result?.b64) {
const fallback = [
"Crée une image de pochette d'album 1024x1024, abstraite, moderne et colorée.",
"Utilise des formes géométriques, textures et dégradés.",
"Aucune personne, aucun visage, aucun texte, aucun logo.",
].join("\n");
try {
result = await tryOnce(fallback);
} catch (e2) {
const em2 = e2?.response?.data || e2?.message || e2;
console.error("Imagen predict fallback FR error", em2);
}
}
if (!result?.b64) {
const fallbackEn = [
"Create a 1024x1024 abstract, modern, colorful album cover.",
"Use geometric shapes, textures, and gradients.",
"No people, no faces, no text, no logos.",
].join("\n");
try {
result = await tryOnce(fallbackEn);
} catch (e3) {
const em3 = e3?.response?.data || e3?.message || e3;
console.error("Imagen predict fallback EN error", em3);
}
}
if (!result?.b64) {
throw new Error("NO_IMAGE_OUTPUT");
}
return {
b64: String(result.b64).trim(),
mimeType: result.mimeType || "image/png",
};
};
// Exporter toutes les fonctions // Exporter toutes les fonctions
exports.music = require("./src/music"); exports.music = require("./src/music");
exports.lyrics = require("./src/lyrics"); exports.lyrics = require("./src/lyrics");
exports.cover = require("./src/cover");
+176
View File
@@ -0,0 +1,176 @@
const { onDocumentCreated } = require("firebase-functions/v2/firestore");
const admin = require("firebase-admin");
const logger = require("firebase-functions/logger");
const { generateImage } = require("../index");
const db = admin.firestore();
// Shared core for generating and saving the cover, and updating the project
async function performCoverGeneration(projectId) {
const t0 = Date.now();
const snap = await db.collection("projects").doc(projectId).get();
if (!snap.exists) {
throw new Error("Projet introuvable");
}
const project = snap.data() || {};
const title = project?.title || "";
const lyricsSections = Array.isArray(project?.lyrics)
? project.lyrics
: [
project?.lyrics?.couplet
? { type: "couplet", lyrics: project.lyrics.couplet }
: null,
project?.lyrics?.refrain
? { type: "refrain", lyrics: project.lyrics.refrain }
: null,
].filter(Boolean);
const cfg = project?.musicConfig || {};
const genres = Array.isArray(cfg?.genres) ? cfg.genres : [];
const tempo = cfg?.tempo || "";
const voice = cfg?.voice || "";
const instruments = Array.isArray(cfg?.instruments) ? cfg.instruments : [];
// Construire un bref contexte visuel à partir des paroles
const sampleLyrics = (lyricsSections || [])
.map((s) => (s?.lyrics || "").split("\n").slice(0, 2).join(" "))
.filter(Boolean)
.slice(0, 6)
.join(" | ");
const ambianceParts = [];
if (genres.length) ambianceParts.push(`Genres: ${genres.join(", ")}`);
if (tempo) ambianceParts.push(`Tempo: ${tempo}`);
if (instruments.length)
ambianceParts.push(`Instruments: ${instruments.join(", ")}`);
if (voice) ambianceParts.push(`Vocal: ${voice}`);
const ambiance = ambianceParts.join(" ; ");
const system = `
Tu es un générateur qui ne renvoie que des données texte. Quand on te demande une image, tu renvoies UNIQUEMENT les octets de l'image encodés en base64, sans aucun autre texte, sans JSON, sans balises, sans préfixe data:.
`.trim();
const prompt = `
Crée une image de pochette d'album 1024x1024, moderne, abstraite et impactante, cohérente avec:
- Titre: ${title || "Sans titre"}
- Ambiance / Style: ${ambiance}
Contraintes:
- Style abstrait: formes géométriques, textures, dégradés, couleurs harmonieuses
- Aucun texte, aucun logo, aucune personne ni visage
- Image PNG, sans typographie apparente
`.trim();
// Utiliser generateImage qui renvoie directement le base64 (sans préfixe data:)
logger.info("🎨 [Cover] Calling model", { projectId });
let b64 = "";
let mimeType = "image/png";
try {
const ai = await generateImage({ system, prompt });
if (typeof ai === "string") {
b64 = ai.trim();
} else if (ai && typeof ai === "object") {
if (typeof ai.imageUrl === "string") {
b64 = ai.imageUrl.replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, "").trim();
} else if (typeof ai.b64 === "string") {
b64 = ai.b64.trim();
if (typeof ai.mimeType === "string") mimeType = ai.mimeType;
}
}
} catch (e) {
logger.error("❌ [Cover] generateImage failed", e);
throw e;
}
if (!b64) {
throw new Error("Génération d'image échouée (base64 vide)");
}
// Sauvegarde dans Cloud Storage
const buffer = Buffer.from(b64, "base64");
const bucket = admin.storage().bucket();
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType === "image/webp" ? "webp" : "png";
const path = `musics/${projectId}/cover.${ext}`;
const token = require("crypto").randomUUID();
await bucket.file(path).save(buffer, {
resumable: false,
metadata: {
contentType: mimeType || "image/png",
cacheControl: "public, max-age=31536000",
metadata: { firebaseStorageDownloadTokens: token },
},
});
const coverUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
path,
)}?alt=media&token=${token}`;
// Mettre à jour le projet avec l'URL et le statut
await db.collection("projects").doc(projectId).set(
{
coverUrl,
coverStatus: "GENERATED",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
logger.info("✅ [Cover] Saved", { projectId, path, ms: Date.now() - t0 });
return coverUrl;
}
// Firestore trigger: création d'une tâche de génération de cover
exports.onTaskCreateGenerateCover = onDocumentCreated(
{
timeoutSeconds: 540,
memory: "1GiB",
document: "tasks/{taskId}",
},
async (event) => {
try {
const data = event?.data?.data() || {};
const type = data?.type || "";
const projectId = data?.projectId || null;
if (type !== "cover") {
logger.info("️ [Task] Ignored non-cover task", { type });
return;
}
if (!projectId) throw new Error("projectId manquant dans la task");
logger.info("🎨 [Task] Start cover generation", { projectId });
// Marquer le projet en génération
await db.collection("projects").doc(projectId).set(
{
coverStatus: "GENERATING",
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
const coverUrl = await performCoverGeneration(projectId);
// Marquer la task comme terminée
await event.data.ref.set(
{
status: "DONE",
coverUrl,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
logger.info("✅ [Task] Cover generated", { projectId });
} catch (error) {
logger.error("❌ [Task] Cover generation error", error);
try {
await event.data.ref.set(
{ status: "ERROR", error: error?.message || "Erreur" },
{ merge: true },
);
} catch (_) {}
}
},
);
-15
View File
@@ -1,25 +1,10 @@
import { registerRootComponent } from "expo"; import { registerRootComponent } from "expo";
import messaging from "@react-native-firebase/messaging";
import notifee from "@notifee/react-native";
import App from "./App"; import App from "./App";
import {
displayNotification,
triggerEvent,
} from "./src/providers/NotificationProvider";
// registerRootComponent calls AppRegistry.registerComponent('main', () => App); // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build, // It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately // the environment is set up appropriately
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log("Message handled in the background!", remoteMessage);
await displayNotification(remoteMessage.data);
});
notifee.onBackgroundEvent(triggerEvent);
function HeadlessCheck({ isHeadless }) { function HeadlessCheck({ isHeadless }) {
if (isHeadless) { if (isHeadless) {
// App has been launched in the background by iOS, ignore // App has been launched in the background by iOS, ignore
+1 -1
View File
@@ -13,7 +13,7 @@ install! 'cocoapods',
prepare_react_native_project! prepare_react_native_project!
target 'minuitstarter' do target 'MusicLand' do
use_expo_modules! use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
+7 -1486
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "minuitstarter.app"
BlueprintName = "minuitstarter"
ReferencedContainer = "container:minuitstarter.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "minuitstarterTests.xctest"
BlueprintName = "minuitstarterTests"
ReferencedContainer = "container:minuitstarter.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "minuitstarter.app"
BlueprintName = "minuitstarter"
ReferencedContainer = "container:minuitstarter.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "minuitstarter.app"
BlueprintName = "minuitstarter"
ReferencedContainer = "container:minuitstarter.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:minuitstarter.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
-7
View File
@@ -1,7 +0,0 @@
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>
@interface AppDelegate : EXAppDelegateWrapper
@end
-66
View File
@@ -1,66 +0,0 @@
#import "AppDelegate.h"
#import <Firebase/Firebase.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
self.moduleName = @"main";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
}
// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
@end
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>943006074419-h43c7p48nhn5f4td4oimv4dq7b1p3s29.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.943006074419-h43c7p48nhn5f4td4oimv4dq7b1p3s29</string>
<key>API_KEY</key>
<string>AIzaSyAkl2RAzUCYTQCcYhSeij6-tXNk6z7121Y</string>
<key>GCM_SENDER_ID</key>
<string>943006074419</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.minuit.starter</string>
<key>PROJECT_ID</key>
<string>minuitcloud</string>
<key>STORAGE_BUCKET</key>
<string>minuitcloud.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:943006074419:ios:5deb4cce66c04c09658546</string>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

@@ -1,14 +0,0 @@
{
"images": [
{
"filename": "App-Icon-1024x1024@1x.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "expo"
}
}
@@ -1,20 +0,0 @@
{
"colors": [
{
"color": {
"components": {
"alpha": "1.000",
"blue": "0.0784313725490196",
"green": "0.0470588235294118",
"red": "0.0588235294117647"
},
"color-space": "srgb"
},
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
@@ -1,23 +0,0 @@
{
"images": [
{
"idiom": "universal",
"filename": "image.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "image@2x.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "image@3x.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

-106
View File
@@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>minuit.starter</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2024.04.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>minuit</string>
<string>com.minuit.starter</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.943006074419-h43c7p48nhn5f4td4oimv4dq7b1p3s29</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+minuitstarter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>itms-apps</string>
<string>minuit</string>
</array>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Nous avons besoin d'accéder à votre appareil photo pour vous permettre de prendre des photos de vos tâches.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSMicrophoneUsageDescription</key>
<string>Nous avons besoin d'accéder à votre microphone pour vous permettre d'enregistrer des messages vocaux pour vos tâches.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Nous avons besoin d'accéder à votre galerie pour vous permettre d'ajouter des photos à vos tâches.</string>
<key>RCTRootViewBackgroundColor</key>
<integer>4279176212</integer>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
-50
View File
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>0A2A.1</string>
<string>3B52.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
<string>C56D.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
<string>85F4.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
-44
View File
@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EXPO-SCENE-1">
<objects>
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
<rect key="frame" x="0" y="0" width="414" height="736"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
<constraints>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
</constraints>
<color key="backgroundColor" name="SplashScreenBackground"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="SplashScreenLogo" width="414" height="736"/>
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="0.0784313725490196" green="0.0470588235294118" red="0.0588235294117647" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
</resources>
</document>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
<key>EXUpdatesEnabled</key>
<false/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
</dict>
</plist>
-10
View File
@@ -1,10 +0,0 @@
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
@@ -1,3 +0,0 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:minuit.starter</string>
</array>
</dict>
</plist>
-4
View File
@@ -1,4 +0,0 @@
//
// @generated
// A blank Swift file must be created for native modules with Swift files to work correctly.
//
-5
View File
@@ -21,15 +21,10 @@
"@gorhom/bottom-sheet": "^4", "@gorhom/bottom-sheet": "^4",
"@gorhom/portal": "^1.0.14", "@gorhom/portal": "^1.0.14",
"@legendapp/motion": "^2.2.1", "@legendapp/motion": "^2.2.1",
"@notifee/react-native": "^7.7.1",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/datetimepicker": "8.2.0", "@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/hooks": "^3.0.0", "@react-native-community/hooks": "^3.0.0",
"@react-native-community/slider": "4.5.5", "@react-native-community/slider": "4.5.5",
"@react-native-firebase/app": "^21.6.1",
"@react-native-firebase/crashlytics": "^21.6.1",
"@react-native-firebase/firestore": "^21.6.1",
"@react-native-firebase/messaging": "^21.6.1",
"@react-native-masked-view/masked-view": "^0.3.2", "@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^6.0.9", "@react-navigation/bottom-tabs": "^6.0.9",
"@react-navigation/core": "^6.1.0", "@react-navigation/core": "^6.1.0",
+2 -1
View File
@@ -13,9 +13,10 @@ const BorderGradientButton = ({
titleStyle, titleStyle,
containerStyle = {}, containerStyle = {},
tint = "dark", tint = "dark",
disabled = false,
}) => { }) => {
return ( return (
<Pressable onPress={onPress} style={{ ...containerStyle }}> <Pressable onPress={onPress} disabled={disabled} style={{ ...containerStyle, opacity: disabled ? 0.6 : 1 }}>
<BorderGradient <BorderGradient
gradientProps={{ gradientProps={{
colors: ["#F94697", "#7023F7"], colors: ["#F94697", "#7023F7"],
+41 -11
View File
@@ -3,6 +3,7 @@ import { View, Text, Image, Pressable, StyleSheet } from "react-native";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import Switch from "../components/Switch"; import Switch from "../components/Switch";
import OptionSelector from "../components/OptionSelector";
import { Container, Title, Input, Button } from "../components/Dialog"; import { Container, Title, Input, Button } from "../components/Dialog";
import { Fonts, Style, gutters } from "../styles"; import { Fonts, Style, gutters } from "../styles";
@@ -12,6 +13,15 @@ const labelOptions = {
name: "Nouveau nom", name: "Nouveau nom",
email: "Nouvelle adresse email", email: "Nouvelle adresse email",
password: "Nouveau mot de passe", password: "Nouveau mot de passe",
language: "Nouvelle langue",
};
const languageOptions = {
fr: "Français",
en: "English",
es: "Español",
de: "Deutsch",
it: "Italiano",
}; };
export default ({ itemKey, type, value, title, onUpdateValue }) => { export default ({ itemKey, type, value, title, onUpdateValue }) => {
@@ -51,7 +61,11 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
}, },
})} })}
> >
{itemKey === "password" ? "********" : value} {itemKey === "password"
? "********"
: itemKey === "language"
? languageOptions[value] || value
: value}
</Text> </Text>
<Image <Image
@@ -84,20 +98,36 @@ export default ({ itemKey, type, value, title, onUpdateValue }) => {
value={currentPassword} value={currentPassword}
onChangeText={(text) => setCurrentPassword(text)} onChangeText={(text) => setCurrentPassword(text)}
keyboardType="visible-password" keyboardType="visible-password"
type={"password"}
containerStyle={{ marginBottom: gutters / 2 }}
/>
)}
{itemKey === "language" ? (
<OptionSelector
optionTypeList={languageOptions}
selected={inputData || "fr"}
setSelected={setInputData}
containerStyle={{ marginBottom: gutters / 2 }}
colorMap={{
fr: { primary: "#F94697", secondary: "#F946971A" },
en: { primary: "#7023F7", secondary: "#7023F71A" },
es: { primary: "#FDBA74", secondary: "#FDBA741A" },
de: { primary: "#60A5FA", secondary: "#60A5FA1A" },
it: { primary: "#34D399", secondary: "#34D3991A" },
}}
/>
) : (
<Input
label={labelOptions[itemKey]}
value={inputData}
onChangeText={(text) => setInputData(text)}
autoCapitalize={
["password", "email"].includes(itemKey) ? "none" : "words"
}
type={itemKey} type={itemKey}
containerStyle={{ marginBottom: gutters / 2 }} containerStyle={{ marginBottom: gutters / 2 }}
/> />
)} )}
<Input
label={labelOptions[itemKey]}
value={inputData}
onChangeText={(text) => setInputData(text)}
autoCapitalize={
["password", "email"].includes(itemKey) ? "none" : "words"
}
type={itemKey}
containerStyle={{ marginBottom: gutters / 2 }}
/>
<Button <Button
label="Valider" label="Valider"
onPress={() => { onPress={() => {
+12 -1
View File
@@ -12,7 +12,7 @@ import { FONT_FAMILY } from "../styles/Fonts";
const INITIAL_BOX_SIZE = 6; const INITIAL_BOX_SIZE = 6;
export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => { export default ({ value, maxValue, progress, onSeek, onSeekStart, onSeekEnd, seekEnabled = false }) => {
const offset = useSharedValue(0); const offset = useSharedValue(0);
const boxWidth = useSharedValue(INITIAL_BOX_SIZE); const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
const [layout, setLayout] = useState(null); const [layout, setLayout] = useState(null);
@@ -22,6 +22,12 @@ export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
const pan = Gesture.Pan() const pan = Gesture.Pan()
.enabled(seekEnabled) .enabled(seekEnabled)
.onBegin(() => {
if (seekEnabled && typeof onSeekStart === 'function') {
// Notify JS thread that user started seeking (e.g., pause audio)
runOnJS(onSeekStart)();
}
})
.onChange((event) => { .onChange((event) => {
offset.value = offset.value =
Math.abs(offset.value) <= MAX_VALUE Math.abs(offset.value) <= MAX_VALUE
@@ -40,6 +46,11 @@ export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0; const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
// Reanimated -> JS thread bridge // Reanimated -> JS thread bridge
runOnJS(onSeek)(ratio); runOnJS(onSeek)(ratio);
})
.onFinalize(() => {
if (seekEnabled && typeof onSeekEnd === 'function') {
runOnJS(onSeekEnd)();
}
}); });
// Reflect external progress into the slider UI // Reflect external progress into the slider UI
+53 -2
View File
@@ -1,17 +1,68 @@
import { View, Text } from "react-native"; import { View, Text } from "react-native";
import React from "react"; import React, { useGlobal } from "reactn";
import AppActionSheet from "../AppActionSheet"; import AppActionSheet from "../AppActionSheet";
import BorderGradientButton from "../BorderGradientButton"; import BorderGradientButton from "../BorderGradientButton";
import GradientButton from "../GradientButton"; import GradientButton from "../GradientButton";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import alert from "../Alert";
import firebase, { usersRef } from "../../config/firebase";
import { reset } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const DeleteAccountModal = () => { const DeleteAccountModal = () => {
const [currentUID] = useGlobal("currentUID");
const [, setTooltip] = useGlobal("_tooltip");
const onClose = async () => { const onClose = async () => {
await SheetManager.hide("DeleteAccount"); await SheetManager.hide("DeleteAccount");
}; };
const confirmDelete = () => {
alert(
"Êtes-vous sûr ?",
"Cette action supprimera votre profil.",
[
{ text: "Annuler", style: "cancel", onPress: () => {} },
{
text: "Confirmer",
onPress: () => {
alert(
"Confirmation",
"Dernière vérification: supprimer définitivement votre profil ?",
[
{ text: "Non", style: "cancel", onPress: () => {} },
{
text: "Oui, supprimer",
onPress: async () => {
try {
if (!currentUID) return;
await usersRef.doc(currentUID).delete();
// Sign out and reset navigation to Login
await firebase.auth().signOut();
reset({ index: 0, routes: [{ name: Routes.Login }] });
setTooltip({ type: "success", text: "Profil supprimé" });
} catch (e) {
setTooltip({
type: "error",
text: e?.message || "Suppression impossible",
});
} finally {
onClose();
}
},
},
],
{ cancelable: true },
);
},
},
],
{ cancelable: true },
);
};
return ( return (
<AppActionSheet id="DeleteAccount"> <AppActionSheet id="DeleteAccount">
<View style={{ gap: 20 }}> <View style={{ gap: 20 }}>
@@ -39,7 +90,7 @@ const DeleteAccountModal = () => {
</Text> </Text>
</View> </View>
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}> <View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<BorderGradientButton title="Oui, supprimer" onPress={onClose} /> <BorderGradientButton title="Oui, supprimer" onPress={confirmDelete} />
<GradientButton title="Non, annuler" onPress={onClose} /> <GradientButton title="Non, annuler" onPress={onClose} />
</View> </View>
</View> </View>
+12
View File
@@ -33,9 +33,21 @@ if (!firebase?.apps?.filter(({ name_ }) => name_ === "[DEFAULT]").length) {
console.log("Firebase init"); console.log("Firebase init");
} }
// Firestore settings for React Native/iOS: avoid streaming transport issues
const firestore = firebase.firestore(); const firestore = firebase.firestore();
try {
// Prefer auto-detect; disable fetch streams for RN iOS stability
firebase.firestore().settings({
experimentalAutoDetectLongPolling: true,
useFetchStreams: false,
ignoreUndefinedProperties: true,
});
} catch (e) {
// Ignore if settings were already set elsewhere
}
export const usersRef = firestore.collection("users"); export const usersRef = firestore.collection("users");
export const projectsRef = firestore.collection("projects");
export const notificationsRef = firestore.collection("notifications"); export const notificationsRef = firestore.collection("notifications");
export const documentsRef = firestore.collection("documents"); export const documentsRef = firestore.collection("documents");
export const chatsRef = firestore.collection("chats"); export const chatsRef = firestore.collection("chats");
+24
View File
@@ -0,0 +1,24 @@
import { useGlobal } from "reactn";
import firebase from "../config/firebase";
import useDataFromRef from "./useDataFromRef";
export default function useUserLikedProjects() {
const [currentUID] = useGlobal("currentUID");
const { data, loading } = useDataFromRef({
ref:
currentUID
? firebase
.firestore()
.collection("projects")
.where("likedBy", "array-contains", currentUID)
: null,
simpleRef: false,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
});
return { projects: Array.isArray(data) ? data : [], loading };
}
+3 -3
View File
@@ -59,7 +59,7 @@ import Language from "../screens/Profile/Language";
import Reels from "../screens/Profile/Reels"; import Reels from "../screens/Profile/Reels";
import Register from "../screens/Register"; import Register from "../screens/Register";
import CreatePassword from "../screens/CreatePassword"; import CreatePassword from "../screens/CreatePassword";
import CreateName from "../screens/CreateName"; import CreatePseudo from "../screens/CreatePseudo";
const screenOptions = { const screenOptions = {
headerShown: false, headerShown: false,
@@ -279,8 +279,8 @@ const screens = [
component: CreatePassword, component: CreatePassword,
}, },
{ {
name: Routes.CreateName, name: Routes.CreatePseudo,
component: CreateName, component: CreatePseudo,
}, },
]; ];
+1 -1
View File
@@ -5,7 +5,7 @@ export const Routes = {
ForgotPassword: "ForgotPassword", ForgotPassword: "ForgotPassword",
Register: "Register", Register: "Register",
CreatePassword: "CreatePassword", CreatePassword: "CreatePassword",
CreateName: "CreateName", CreatePseudo: "CreatePseudo",
BottomTab: "BottomTab", BottomTab: "BottomTab",
Welcome: "Welcome", Welcome: "Welcome",
-155
View File
@@ -1,155 +0,0 @@
import {
createContext,
useContext,
useEffect,
useGlobal,
setGlobal,
} from "reactn";
import messaging from "@react-native-firebase/messaging";
import notifee, { EventType } from "@notifee/react-native";
import { CommonActions } from "@react-navigation/core";
import { useAppState } from "@react-native-community/hooks";
import { Linking } from "react-native";
import { dispatch } from "../navigation/NavigationService";
import { Routes } from "../navigation";
import firebase, { usersRef } from "../config/firebase";
import alert from "../components/Alert";
export const NotificationContext = createContext();
export async function displayNotification(data) {
const { title, message, picture = null, video = null } = data || {};
let ios = {
categoryId: "default",
};
if (picture) {
ios.attachments = [{ url: picture }];
}
if (video) {
ios.attachments = [
{
url: video,
thumbnailTime: data?.thumbnailTime
? parseInt(data?.thumbnailTime, 10)
: 0,
},
];
}
await notifee.displayNotification({
title,
body: message,
ios,
android: {
channelId: "default",
},
data,
});
await notifee.incrementBadgeCount();
}
export async function triggerEvent({ type, detail }) {
const { notification } = detail;
if (type === EventType.PRESS) {
const { data = {} } = notification || {};
const { projectID = null, taskID = null, messageID = null } = data || {};
if (projectID) {
setGlobal({ currentProjectID: projectID });
if (taskID) {
dispatch(
CommonActions.navigate({
name: Routes.TaskDetails,
params: { taskID, messageID },
})
);
}
}
}
}
export default ({ children }) => {
const [currentUID] = useGlobal("currentUID");
const currentAppState = useAppState();
useEffect(() => {
if (currentUID) {
updateFCMToken({ uid: currentUID });
}
}, [currentUID]);
useEffect(() => {
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
console.log("Message handled in the foreground!", remoteMessage);
const { data } = remoteMessage;
await displayNotification(data);
});
const foregroundSubscription = notifee.onForegroundEvent(triggerEvent);
return () => {
unsubscribe();
foregroundSubscription();
};
}, []);
useEffect(() => {
if (currentAppState === "active") {
notifee.setBadgeCount(0);
}
}, [currentAppState]);
const updateFCMToken = async ({ uid }) => {
try {
const authResponse = await messaging().requestPermission();
const enabled = authResponse === messaging.AuthorizationStatus.AUTHORIZED;
if (enabled) {
const fcmToken = await messaging().getToken();
if (fcmToken) {
await usersRef.doc(uid).update({
fcmTokens: firebase.firestore.FieldValue.arrayUnion(fcmToken),
});
}
} else {
alert(
"Activez les notifications",
"Afin de rester informé de l'avancée de votre projet, il est important d'activer les notifications.",
[
{
text: "Annuler",
onPress: () => {},
style: "cancel",
},
{
text: "Paramètres",
onPress: async () => {
Linking.openSettings();
},
style: "confirm",
},
]
);
}
} catch (error) {
console.log("error", error);
}
};
return (
<NotificationContext.Provider value={{}}>
{children}
</NotificationContext.Provider>
);
};
export const useNotification = () => {
return useContext(NotificationContext);
};
-56
View File
@@ -1,56 +0,0 @@
import React, { useContext, createContext, useEffect, useState } from "react";
import firebase from "../config/firebase";
import { useUserData } from "./UserDataProvider";
export const PremiumContext = createContext();
export default ({ children }) => {
const { currentUID } = useUserData();
const [subscriptions, setSubscriptions] = useState(null);
const subscriptionType = subscriptions?.length > 0 ? "PREMIUM" : "FREE";
const isPaying = subscriptionType !== "FREE";
useEffect(() => {
if (currentUID) {
getPremiumStatus();
}
}, [currentUID]);
const getPremiumStatus = async () => {
try {
const { data } = await firebase
.functions()
.httpsCallable("payment-getPremiumStatus")({
userID: currentUID,
});
if (data?.subscriptions?.length) {
setSubscriptions(
(data.subscriptions || [])?.filter((sub) => sub.status === "active")
);
}
} catch (error) {
console.log(error);
}
};
return (
<PremiumContext.Provider
value={{
getPremiumStatus,
subscriptionType,
subscriptions,
isPaying,
}}
>
{children}
</PremiumContext.Provider>
);
};
export const usePremium = () => useContext(PremiumContext);
-2
View File
@@ -6,7 +6,6 @@ import UserDataProvider from "./UserDataProvider";
import WebViewProvider from "./WebViewProvider"; import WebViewProvider from "./WebViewProvider";
import SplashAnimationProvider from "./SplashAnimationProvider"; import SplashAnimationProvider from "./SplashAnimationProvider";
import UniversalLinkProvider from "./UniversalLinkProvider"; import UniversalLinkProvider from "./UniversalLinkProvider";
import PremiumProvider from "./PremiumProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider"; import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
import { SheetProvider } from "react-native-actions-sheet"; import { SheetProvider } from "react-native-actions-sheet";
@@ -18,7 +17,6 @@ const SharedProviders = ({ children }) => {
[WebViewProvider, {}], [WebViewProvider, {}],
[UserDataProvider, {}], [UserDataProvider, {}],
[StripeEmbeddedProvider, {}], [StripeEmbeddedProvider, {}],
[PremiumProvider, {}],
[UniversalLinkProvider, {}], [UniversalLinkProvider, {}],
[SheetProvider, {}], [SheetProvider, {}],
]; ];
+41
View File
@@ -18,6 +18,8 @@ export default ({ children }) => {
const [currentUID] = useGlobal("currentUID"); const [currentUID] = useGlobal("currentUID");
const [currentUserData, setCurrentUserData] = useGlobal("currentUserData"); const [currentUserData, setCurrentUserData] = useGlobal("currentUserData");
const [currentUserRoles] = useGlobal("currentUserRoles"); const [currentUserRoles] = useGlobal("currentUserRoles");
const [userProjects, setUserProjects] = useState([]);
const [userLikedProjects, setUserLikedProjects] = useState([]);
useDataFromRef({ useDataFromRef({
ref: currentUID ? usersRef.doc(currentUID) : null, ref: currentUID ? usersRef.doc(currentUID) : null,
@@ -34,6 +36,38 @@ export default ({ children }) => {
}, },
}); });
// Subscribe to user's projects (musics)
useDataFromRef({
ref: currentUID
? firebase
.firestore()
.collection("projects")
.where("userId", "==", currentUID)
.orderBy("updatedAt", "desc")
: null,
simpleRef: false,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
onUpdate: (list) => setUserProjects(Array.isArray(list) ? list : []),
});
// Subscribe to user's liked projects
useDataFromRef({
ref: currentUID
? firebase
.firestore()
.collection("projects")
.where("likedBy", "array-contains", currentUID)
.orderBy("updatedAt", "desc")
: null,
simpleRef: false,
listener: true,
condition: !!currentUID,
refreshArray: [currentUID],
onUpdate: (list) => setUserLikedProjects(Array.isArray(list) ? list : []),
});
const updatePendingUserData = (newData) => { const updatePendingUserData = (newData) => {
setPendingUserData((prevData) => ({ setPendingUserData((prevData) => ({
...prevData, ...prevData,
@@ -144,6 +178,8 @@ export default ({ children }) => {
currentUserRoles, currentUserRoles,
currentUID, currentUID,
currentUserData, currentUserData,
userProjects,
userLikedProjects,
setCurrentUserData, setCurrentUserData,
@@ -163,3 +199,8 @@ export default ({ children }) => {
export const useUserData = () => { export const useUserData = () => {
return useContext(UserDataContext); return useContext(UserDataContext);
}; };
// Convenience alias returning the same context, as requested
export const useUser = () => {
return useContext(UserDataContext);
};
+1 -4
View File
@@ -2,7 +2,6 @@ import React from "react";
import { MinuitProvider } from "react-native-minuit"; import { MinuitProvider } from "react-native-minuit";
import SharedProviders from "./SharedProviders"; import SharedProviders from "./SharedProviders";
import NotificationProvider from "./NotificationProvider";
import { Palette } from "../styles"; import { Palette } from "../styles";
@@ -16,9 +15,7 @@ export default ({ children }) => {
destructive: "red", destructive: "red",
}} }}
> >
<SharedProviders> <SharedProviders>{children}</SharedProviders>
<NotificationProvider>{children}</NotificationProvider>
</SharedProviders>
</MinuitProvider> </MinuitProvider>
); );
}; };
+23 -1
View File
@@ -45,6 +45,18 @@ export default ({ navigation }) => {
value: "", value: "",
type: "string", type: "string",
}, },
{
title: "Notifications",
key: "notifications",
value: !!currentUserData?.notifications,
type: "boolean",
},
{
title: "Langue",
key: "language",
value: currentUserData?.language || "",
type: "string",
},
]; ];
const destructiveSettingsList = [ const destructiveSettingsList = [
@@ -125,7 +137,11 @@ export default ({ navigation }) => {
} }
}; };
if (!value) { // Validate input except for booleans where false is allowed
const isInvalidString =
typeof value === "string" && value.trim().length === 0;
const isNullish = value === null || value === undefined;
if (isNullish || isInvalidString) {
throw new Error("Vérifiez les informations saisies"); throw new Error("Vérifiez les informations saisies");
} }
@@ -269,6 +285,12 @@ export default ({ navigation }) => {
); );
})} })}
<ItemRowList
title={"Gérer mon abonnement"}
action={() => {}}
containerStyle={{}}
/>
{destructiveSettingsList.map((setting, index) => ( {destructiveSettingsList.map((setting, index) => (
<ItemRowList key={index} {...setting} containerStyle={{}} /> <ItemRowList key={index} {...setting} containerStyle={{}} />
))} ))}
-62
View File
@@ -1,62 +0,0 @@
import { View, Text } from "react-native";
import React from "react";
import Page from "../layouts/Page";
import { background } from "../assets";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { Input } from "../components/Input";
import GradientButton from "../components/GradientButton";
import { navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
const CreateName = () => {
return (
<Page
backgroundImg={background.homeBG}
headerType="NAVIGATION"
title="Inscription"
>
<View style={{ flex: 1, paddingTop: 20 }}>
<ItemContainer height={275}>
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
<View style={{ gap: 16 }}>
<View style={{ gap: 2 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
Comment tappelles-tu?
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Invisible pour les autres, sauf si tu lutilises comme nom
dartiste.
</Text>
</View>
<Input placeholder="Prénom" label="Prénom" isBlur />
</View>
<GradientButton
title="Suivant"
containerStyle={{
width: "80%",
alignSelf: "center",
}}
onPress={() => navigate(Routes.Login)}
/>
</View>
</ItemContainer>
</View>
</Page>
);
};
export default CreateName;
+41 -3
View File
@@ -1,5 +1,6 @@
import { View, Text, Pressable } from "react-native"; import { View, Text, Pressable } from "react-native";
import React from "react"; import React, { useState } from "react";
import { useGlobal } from "reactn";
import Page from "../layouts/Page"; import Page from "../layouts/Page";
import { background } from "../assets"; import { background } from "../assets";
import ItemContainer from "../components/ItemContainer/ItemContainer"; import ItemContainer from "../components/ItemContainer/ItemContainer";
@@ -8,9 +9,43 @@ import { FONT_FAMILY } from "../styles/Fonts";
import { Input } from "../components/Input"; import { Input } from "../components/Input";
import GradientButton from "../components/GradientButton"; import GradientButton from "../components/GradientButton";
import { navigate } from "../navigation/NavigationService"; import { navigate } from "../navigation/NavigationService";
import { useRoute } from "@react-navigation/native";
import firebase, { usersRef } from "../config/firebase";
import { Routes } from "../navigation"; import { Routes } from "../navigation";
const CreatePassword = () => { const CreatePassword = () => {
const route = useRoute();
const email = route?.params?.email || "";
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const onCreateAccount = async () => {
try {
setLoading(true);
const cred = await firebase
.auth()
.createUserWithEmailAndPassword(email.trim(), password);
const uid = cred?.user?.uid || firebase.auth().currentUser?.uid;
if (!uid) throw new Error("Création de compte échouée");
await usersRef.doc(uid).set(
{
email: email.trim(),
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
setTooltip({ text: "Compte créé, continuons", type: "success" });
navigate(Routes.CreatePseudo);
} catch (e) {
console.log("Register error", e?.message);
setTooltip({ text: e?.message || "Création impossible", type: "error" });
} finally {
setLoading(false);
}
};
return ( return (
<Page <Page
backgroundImg={background.homeBG} backgroundImg={background.homeBG}
@@ -46,15 +81,18 @@ const CreatePassword = () => {
label="Mot de passe" label="Mot de passe"
isBlur isBlur
type="password" type="password"
value={password}
setValue={setPassword}
/> />
</View> </View>
<GradientButton <GradientButton
title="Suivant" title={loading ? "Création..." : "Suivant"}
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={() => navigate(Routes.CreateName)} onPress={onCreateAccount}
disabled={loading || !email || !password}
/> />
</View> </View>
</ItemContainer> </ItemContainer>
+111
View File
@@ -0,0 +1,111 @@
import { View, Text } from "react-native";
import React, { useState } from "react";
import { useGlobal } from "reactn";
import Page from "../layouts/Page";
import { background } from "../assets";
import ItemContainer from "../components/ItemContainer/ItemContainer";
import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { Input } from "../components/Input";
import GradientButton from "../components/GradientButton";
import { navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
import firebase, { usersRef } from "../config/firebase";
const CreatePseudo = () => {
const [userName, setUserName] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const onCreate = async () => {
const raw = userName || "";
const val = raw.trim();
if (!val) return;
try {
setLoading(true);
const uid = firebase.auth().currentUser?.uid;
if (!uid) {
return navigate(Routes.Login);
}
// Check uniqueness (case-insensitive)
const lower = val.toLowerCase();
const snap = await usersRef.where("userNameLower", "==", lower).limit(1).get();
if (!snap.empty && snap.docs[0].id !== uid) {
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
return;
}
await usersRef.doc(uid).set(
{
userName: val,
userNameLower: lower,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
setTooltip({ text: `Bienvenue ${val}`, type: "success" });
navigate(Routes.BottomTab);
} catch (e) {
console.log("CreatePseudo error", e?.message);
setTooltip({ text: e?.message || "Enregistrement impossible", type: "error" });
} finally {
setLoading(false);
}
};
return (
<Page
backgroundImg={background.homeBG}
headerType="NAVIGATION"
title="Ton pseudo"
hideBackButton
>
<View style={{ flex: 1, paddingTop: 20 }}>
<ItemContainer height={275}>
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
<View style={{ gap: 16 }}>
<View style={{ gap: 2 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
Choisis ton pseudo
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Visible par les autres utilisateurs.
</Text>
</View>
<Input
placeholder="Pseudo"
label="Pseudo"
isBlur
value={userName}
setValue={setUserName}
/>
</View>
<GradientButton
title={loading ? "Chargement..." : "Valider"}
containerStyle={{
width: "80%",
alignSelf: "center",
}}
onPress={onCreate}
disabled={loading || !userName?.trim()}
/>
</View>
</ItemContainer>
</View>
</Page>
);
};
export default CreatePseudo;
+26 -6
View File
@@ -1,5 +1,5 @@
import { View, Text, Image, Pressable, Platform } from "react-native"; import { View, Text, Image, Pressable, Platform } from "react-native";
import React, { useState } from "react"; import React, { useMemo, useState } from "react";
import Page from "../layouts/Page"; import Page from "../layouts/Page";
import { ai, background, icons } from "../assets"; import { ai, background, icons } from "../assets";
import { Palette, Style } from "../styles"; import { Palette, Style } from "../styles";
@@ -9,10 +9,29 @@ import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../styles/Fonts"; import { FONT_FAMILY } from "../styles/Fonts";
import { size } from "../styles/Style"; import { size } from "../styles/Style";
import GradientButton from "../components/GradientButton"; import GradientButton from "../components/GradientButton";
import { goBack } from "../navigation/NavigationService"; import { goBack, navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation";
import { useUser } from "../providers/UserDataProvider";
const CreateSongs = () => { const CreateSongs = () => {
const [selectedIndex, setSelectedIndex] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null);
const { userProjects = [] } = useUser();
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects.slice(0, 3) : []),
[userProjects],
);
const fmtDate = (ts) => {
try {
const d = ts?.toDate ? ts.toDate() : ts ? new Date(ts) : null;
if (!d || Number.isNaN(d.getTime())) return "";
const dd = `${d.getDate().toString().padStart(2, "0")}/${(d.getMonth() + 1)
.toString()
.padStart(2, "0")}`;
return dd;
} catch (_) {
return "";
}
};
return ( return (
<Page backgroundImg={background.homeBG} headerType="NONE"> <Page backgroundImg={background.homeBG} headerType="NONE">
@@ -102,7 +121,7 @@ const CreateSongs = () => {
</Text> </Text>
</View> </View>
<View style={{ gap: 10, zIndex: 1 }}> <View style={{ gap: 10, zIndex: 1 }}>
{Array.from({ length: 3 }).map((item, index) => ( {projects.map((item, index) => (
<Pressable <Pressable
key={index} key={index}
onPress={() => { onPress={() => {
@@ -140,7 +159,7 @@ const CreateSongs = () => {
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}
> >
Chanson sans titre {item?.title || "Sans titre"}
</Text> </Text>
<Text <Text
style={{ style={{
@@ -152,7 +171,7 @@ const CreateSongs = () => {
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}
> >
Modifié le 23/04 {`Modifié le ${fmtDate(item?.updatedAt || item?.createdAt)}`}
</Text> </Text>
</View> </View>
<Pressable> <Pressable>
@@ -175,12 +194,13 @@ const CreateSongs = () => {
))} ))}
</View> </View>
<GradientButton <GradientButton
title="Créer un texte" title={selectedIndex !== null ? "Poursuivre la création" : "Créer un texte"}
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
marginTop: 10, marginTop: 10,
}} }}
onPress={() => navigate(Routes.Writing)}
/> />
</BlurView> </BlurView>
</BorderGradient> </BorderGradient>
+25 -2
View File
@@ -9,9 +9,23 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import SongCard from "./components/SongCard"; import SongCard from "./components/SongCard";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import PlaybacksCard from "./components/PlaybacksCard"; import PlaybacksCard from "./components/PlaybacksCard";
import firebase from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const HitParade = () => { const HitParade = () => {
const [selected, setSelected] = useState("Chansons"); const [selected, setSelected] = useState("Chansons");
const { data: latestSongs } = useDataFromRef({
ref: firebase
.firestore()
.collection("projects")
.orderBy("createdAt", "desc")
.limit(10),
simpleRef: false,
listener: true,
condition: true,
});
return ( return (
<Page backgroundImg={background.hitParadeBG} headerType="NONE"> <Page backgroundImg={background.hitParadeBG} headerType="NONE">
@@ -80,8 +94,17 @@ const HitParade = () => {
gap: 10, gap: 10,
paddingBottom: responsiveHeight(20), paddingBottom: responsiveHeight(20),
}} }}
data={Array.from({ length: 5 })} data={Array.isArray(latestSongs) ? latestSongs : []}
renderItem={() => <SongCard />} keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<SongCard
rank={index + 1}
title={item?.title || "Sans titre"}
artist={"MusicLand"}
coverUrl={item?.coverUrl || null}
onPress={() => navigate(Routes.MusicDetails, { projectId: item.id })}
/>
)}
/> />
)} )}
{selected === "Playbacks" && ( {selected === "Playbacks" && (
+9 -8
View File
@@ -1,4 +1,4 @@
import { View, Text, Image, Platform } from "react-native"; import { View, Text, Image, Platform, Pressable } from "react-native";
import React from "react"; import React from "react";
import { img } from "../../../assets"; import { img } from "../../../assets";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
@@ -7,16 +7,17 @@ import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts"; import { FONT_FAMILY } from "../../../styles/Fonts";
import CoinBadge from "./CoinBadge"; import CoinBadge from "./CoinBadge";
const SongCard = () => { const SongCard = ({ rank = 1, title = "Sans titre", artist = "MusicLand", coverUrl = null, onPress = null }) => {
return ( return (
<View <Pressable
onPress={onPress || undefined}
style={{ style={{
...Style.containerRow, ...Style.containerRow,
gap: 6, gap: 6,
}} }}
> >
<Image <Image
source={img.placeholder2} source={coverUrl ? { uri: coverUrl } : img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }} style={{ ...size({ size: 60 }), borderRadius: 12 }}
/> />
<BlurView <BlurView
@@ -43,7 +44,7 @@ const SongCard = () => {
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}} }}
> >
1 {rank}
</Text> </Text>
<View> <View>
<Text <Text
@@ -53,7 +54,7 @@ const SongCard = () => {
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
}} }}
> >
Alors on danse {title}
</Text> </Text>
<Text <Text
style={{ style={{
@@ -62,13 +63,13 @@ const SongCard = () => {
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}
> >
Stromae {artist}
</Text> </Text>
</View> </View>
</View> </View>
<CoinBadge /> <CoinBadge />
</BlurView> </BlurView>
</View> </Pressable>
); );
}; };
+14 -5
View File
@@ -67,11 +67,20 @@ const Home = () => {
{CREATE_DATA.map((item, index) => ( {CREATE_DATA.map((item, index) => (
<Pressable <Pressable
key={index} key={index}
onPress={() => onPress={() => {
navigate(Routes.Create, { switch (index) {
action: item.type, case 0:
}) navigate(Routes.Create, {
} action: item.type,
});
break;
case 1:
navigate(Routes.Studio, {
action: item.type,
});
break;
}
}}
> >
<BlurView <BlurView
tint="dark" tint="dark"
+29 -18
View File
@@ -8,10 +8,14 @@ import Animated, { Easing, FadeIn, FadeOut } from "react-native-reanimated";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import { useUser } from "../../providers/UserDataProvider";
const AllMyLikedMusic = () => { const AllMyLikedMusic = () => {
const [top, setTop] = useState(0); const [top, setTop] = useState(0);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const { userLikedProjects: projects } = useUser();
return ( return (
<Page <Page
@@ -22,26 +26,33 @@ const AllMyLikedMusic = () => {
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
{Array.from({ length: 3 }).map((_, index) => ( {Array.isArray(projects) &&
<MusicCard projects.map((item) => (
key={index} <MusicCard
onPressMore={(posTop) => { key={item.id}
setTop(posTop); title={item?.title || "Sans titre"}
if (showMenu) { imageUri={item?.coverUrl || null}
if (posTop === top) { subtitle={"MusicLand"}
setShowMenu(false); projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() => navigate(Routes.MusicDetails, { projectId: item.id })}
onPressMore={(posTop) => {
setTop(posTop);
if (showMenu) {
if (posTop === top) {
setShowMenu(false);
} else {
setShowMenu(false);
setTimeout(() => {
setShowMenu(true);
}, 300);
}
} else { } else {
setShowMenu(false); setShowMenu(true);
setTimeout(() => {
setShowMenu(true);
}, 300);
} }
} else { }}
setShowMenu(true); />
} ))}
}}
/>
))}
{showMenu && ( {showMenu && (
<Animated.View <Animated.View
entering={FadeIn.duration(300).easing(Easing.ease)} entering={FadeIn.duration(300).easing(Easing.ease)}
+15 -5
View File
@@ -6,10 +6,14 @@ import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import useLayoutType from "../../hooks/useLayoutType"; import useLayoutType from "../../hooks/useLayoutType";
import { useUser } from "../../providers/UserDataProvider";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const AllMyMusic = () => { const AllMyMusic = () => {
const [containerLayout, setContainerLayout] = useState(null); const [containerLayout, setContainerLayout] = useState(null);
const { isWeb } = useLayoutType(); const { isWeb } = useLayoutType();
const { userProjects: projects } = useUser();
return ( return (
<Page <Page
@@ -39,17 +43,23 @@ const AllMyMusic = () => {
}} }}
> >
<FlatList <FlatList
data={Array.from({ length: 9 })} data={Array.isArray(projects) ? projects : []}
numColumns={3} numColumns={3}
columnWrapperStyle={{ gap: 10 }} columnWrapperStyle={{ gap: 10 }}
contentContainerStyle={{ gap: 14 }} contentContainerStyle={{ gap: 14 }}
renderItem={() => ( keyExtractor={(item) => item.id}
// Projects are provided by provider with a live listener
renderItem={({ item }) => (
<Pressable <Pressable
style={{ flex: 1, gap: 4 }} style={{ flex: 1, gap: 4 }}
onPress={() => console.log("PRESSED")} onPress={() =>
navigate(Routes.MusicDetails, { projectId: item.id })
}
> >
<Image <Image
source={img.placeholder2} source={
item?.coverUrl ? { uri: item.coverUrl } : img.placeholder2
}
style={{ width: "100%", height: 114, borderRadius: 8 }} style={{ width: "100%", height: 114, borderRadius: 8 }}
/> />
<Text <Text
@@ -59,7 +69,7 @@ const AllMyMusic = () => {
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
}} }}
> >
Voiture {item?.title || "Sans titre"}
</Text> </Text>
</Pressable> </Pressable>
)} )}
+256 -41
View File
@@ -6,7 +6,7 @@ import {
Pressable, Pressable,
StyleSheet, StyleSheet,
} from "react-native"; } from "react-native";
import React, { useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { background, icons, img } from "../../assets"; import { background, icons, img } from "../../assets";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
@@ -15,16 +15,169 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import Slider from "../../components/Slider"; import Slider from "../../components/Slider";
import { useRoute } from "@react-navigation/core"; import { useRoute } from "@react-navigation/core";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import firebase, { usersRef, projectsRef, arrayUnion, arrayRemove } from "../../config/firebase";
import { useGlobal } from "reactn";
import useDataFromRef from "../../hooks/useDataFromRef";
import { Audio } from "expo-av";
const MusicDetails = () => { const MusicDetails = () => {
const params = useRoute().params; const params = useRoute().params || {};
const action = params?.action; const action = params?.action;
const projectId = params?.projectId || null;
const [fav, setFav] = useState(false); const [fav, setFav] = useState(false);
const [currentUID] = useGlobal("currentUID");
const [isPlaying, setIsPlaying] = useState(false);
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const wasPlayingBeforeSeek = React.useRef(false);
const { data: project } = useDataFromRef({
ref: projectId
? firebase.firestore().collection("projects").doc(projectId)
: null,
simpleRef: true,
listener: true,
condition: !!projectId,
});
const { data: owner } = useDataFromRef({
ref: project?.userId ? usersRef.doc(project.userId) : null,
simpleRef: true,
listener: true,
condition: !!project?.userId,
});
useEffect(() => {
if (project && currentUID) {
const liked = Array.isArray(project?.likedBy)
? project.likedBy.includes(currentUID)
: false;
setFav(liked);
}
}, [project?.likedBy, currentUID]);
const title = project?.title || "Sans titre";
const artist = owner?.userName || "MusicLand";
const coverUrl = project?.coverUrl || null;
const songUrl = useMemo(() => {
if (project?.song?.url) return project.song.url;
const arr = Array.isArray(project?.musicUrls) ? project.musicUrls : [];
return arr[0] || null;
}, [project]);
const soundRef = React.useRef(null);
// Load/unload audio with expo-av for reliable status updates on iOS/Android
useEffect(() => {
let isMounted = true;
const load = async () => {
try {
// Unload previous
if (soundRef.current) {
await soundRef.current.unloadAsync();
soundRef.current.setOnPlaybackStatusUpdate(null);
soundRef.current = null;
}
if (!songUrl) return;
const { sound } = await Audio.Sound.createAsync(
{ uri: songUrl },
{ shouldPlay: false },
(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);
}
};
load();
return () => {
isMounted = false;
(async () => {
try {
if (soundRef.current) {
await soundRef.current.unloadAsync();
soundRef.current.setOnPlaybackStatusUpdate(null);
soundRef.current = null;
}
} catch (_) {}
})();
};
}, [songUrl]);
const fmt = (ms) => {
const total = Math.max(0, Math.floor((ms || 0) / 1000));
const m = Math.floor(total / 60)
.toString()
.padStart(1, "0");
const s = (total % 60).toString().padStart(2, "0");
return `${m}:${s}`;
};
const togglePlay = async () => {
const sound = soundRef.current;
if (!sound || !songUrl) return;
try {
const status = await sound.getStatusAsync();
if (status?.isLoaded && status.isPlaying) {
await sound.pauseAsync();
setIsPlaying(false);
} else {
await sound.playAsync();
setIsPlaying(true);
}
} catch (e) {
console.log("MusicDetails audio error", e?.message);
}
};
const onSeek = async (ratio) => {
try {
const dur = progressInfo.dur || 0;
const pos = Math.floor(dur * ratio);
const sound = soundRef.current;
if (sound && dur > 0) {
await sound.setPositionAsync(pos);
}
} catch (e) {
console.log("MusicDetails seek error", e?.message);
}
};
const seekBy = async (deltaSeconds) => {
try {
const cur = Math.floor((progressInfo.pos || 0) / 1000);
const next = Math.max(0, cur + deltaSeconds);
const sound = soundRef.current;
if (sound) await sound.setPositionAsync(next * 1000);
} catch (e) {
console.log("MusicDetails seekBy error", e?.message);
}
};
const description = useMemo(() => {
// Try to build a readable text from lyrics if present
if (Array.isArray(project?.lyrics)) {
return project.lyrics
.map((s) => (s?.lyrics || "").trim())
.filter(Boolean)
.slice(0, 4)
.join("\n\n");
}
const c = project?.lyrics?.couplet;
const r = project?.lyrics?.refrain;
const parts = [c, r].filter(Boolean);
return parts.length ? parts.join("\n\n") : "";
}, [project]);
return ( return (
<Page <Page
headerType="NAVIGATION" headerType="NAVIGATION"
title={action === "userProfile" ? "Mon profil" : "Recherche"} title={action === "userProfile" ? "Mon profil" : "Détail musique"}
backgroundImg={ backgroundImg={
action === "userProfile" ? background.profileBG : background.libraryBG2 action === "userProfile" ? background.profileBG : background.libraryBG2
} }
@@ -48,14 +201,32 @@ const MusicDetails = () => {
// stickyHeaderIndices={[1]} // stickyHeaderIndices={[1]}
> >
<View style={{ gap: 28 }}> <View style={{ gap: 28 }}>
<Image source={img.placeholder4} style={styles.img} /> <Image
source={coverUrl ? { uri: coverUrl } : img.placeholder4}
style={styles.img}
/>
<View style={{ ...Style.containerSpaceBetween }}> <View style={{ ...Style.containerSpaceBetween }}>
<View> <View>
<Text style={styles.title}>Lust for Life</Text> <Text style={styles.title}>{title}</Text>
<Text style={styles.name}>Lana del Rey</Text> <Text style={styles.name}>{artist}</Text>
</View> </View>
<View style={{ ...Style.containerRow, gap: 12 }}> <View style={{ ...Style.containerRow, gap: 12 }}>
<Pressable onPress={() => setFav(!fav)}> <Pressable
onPress={async () => {
if (!projectId || !currentUID) return;
const next = !fav;
setFav(next);
try {
await projectsRef.doc(projectId).update({
likedBy: next
? arrayUnion(currentUID)
: arrayRemove(currentUID),
});
} catch (e) {
setFav(!next);
}
}}
>
<Image <Image
source={fav ? icons.heart : icons.heartOutline} source={fav ? icons.heart : icons.heartOutline}
style={{ ...size({ size: 24 }) }} style={{ ...size({ size: 24 }) }}
@@ -73,47 +244,91 @@ const MusicDetails = () => {
</View> </View>
</View> </View>
<View style={{ paddingTop: 22 }}> <View style={{ paddingTop: 22 }}>
<Slider value={"0:00"} maxValue={"2:00"} /> <Slider
value={fmt(progressInfo.pos)}
maxValue={fmt(progressInfo.dur)}
progress={
progressInfo.dur ? (progressInfo.pos || 0) / progressInfo.dur : 0
}
seekEnabled={!!songUrl}
onSeekStart={async () => {
try {
const sound = soundRef.current;
const status = await sound?.getStatusAsync?.();
wasPlayingBeforeSeek.current =
!!status?.isLoaded && !!status?.isPlaying;
if (status?.isLoaded && status?.isPlaying) {
await sound.pauseAsync();
setIsPlaying(false);
}
} catch (e) {
console.log("Pause on seek start error", e?.message);
}
}}
onSeek={onSeek}
onSeekEnd={async () => {
try {
const sound = soundRef.current;
if (sound && wasPlayingBeforeSeek.current) {
await sound.playAsync();
setIsPlaying(true);
}
wasPlayingBeforeSeek.current = false;
} catch (e) {
console.log("Resume after seek error", e?.message);
}
}}
/>
<View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}> <View style={{ ...Style.containerRow, gap: 24, alignSelf: "center" }}>
<Pressable> {/* Previous (rewind 10s) */}
<Image source={icons.forward} /> <Pressable onPress={() => seekBy(-10)}>
</Pressable>
<Pressable>
<Image source={icons.pause} />
</Pressable>
<Pressable>
<Image <Image
source={icons.forward} source={icons.forward}
style={{ transform: [{ rotate: "180deg" }] }} style={{
...size({ size: 30 }),
}}
/>
</Pressable>
{/* Play / Pause */}
<Pressable
style={{
...size({ size: 40 }),
alignItems: "center",
justifyContent: "center",
}}
onPress={togglePlay}
>
<Image
resizeMode={"contain"}
source={isPlaying ? icons.pause : icons.play}
style={size({ size: 34 })}
/>
</Pressable>
{/* Next (forward 10s) */}
<Pressable onPress={() => seekBy(10)}>
<Image
source={icons.forward}
style={{
...size({ size: 30 }),
transform: [{ rotate: "180deg" }],
}}
/> />
</Pressable> </Pressable>
</View> </View>
</View> </View>
<View style={{ marginTop: 30, gap: 20 }}> {description?.length > 0 && (
<Text <View style={{ marginTop: 30, gap: 20 }}>
style={{ <Text
fontSize: 14, style={{
color: Palette.white, fontSize: 16,
fontFamily: FONT_FAMILY.InterRegular, color: Palette.white,
}} fontFamily: FONT_FAMILY.InterRegular,
> }}
Description chanson. Viverra enim risus enim enim placerat. Integer >
pulvinar tristique suscipit risus. Id hendrerit in odio phasellus {description}
interdum lectus amet </Text>
</Text> </View>
<Text )}
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Allô{"\n"}Écoute maman est près de toi{"\n"}Il faut lui dire "maman,
c'est quelqu'un pour toi"{"\n"}Ah, c'est le monsieur de la dernière
fois{"\n"}Bon, je vais la chercher{"\n"}Je crois qu'elle est dans
son bain{"\n"}Et je sais pas si elle va pouvoir venir
</Text>
</View>
</ScrollView> </ScrollView>
</Page> </Page>
); );
+15 -6
View File
@@ -1,12 +1,17 @@
import { View, Text, Image, FlatList } from "react-native"; import { View, Image, FlatList, Pressable } from "react-native";
import React from "react"; import React from "react";
import CardContainer from "./CardContainer"; import CardContainer from "./CardContainer";
import { Style } from "../../../styles"; import { Style } from "../../../styles";
import { img } from "../../../assets"; import { img } from "../../../assets";
import { navigate } from "../../../navigation/NavigationService"; import { navigate } from "../../../navigation/NavigationService";
import { Routes } from "../../../navigation"; import { Routes } from "../../../navigation";
import { useUser } from "../../../providers/UserDataProvider";
const LikedMusic = () => { const LikedMusic = () => {
const { userLikedProjects = [] } = useUser();
const items = Array.isArray(userLikedProjects)
? userLikedProjects.slice(0, 6)
: [];
return ( return (
<CardContainer <CardContainer
label="Musiques likées" label="Musiques likées"
@@ -14,17 +19,21 @@ const LikedMusic = () => {
> >
<FlatList <FlatList
scrollEnabled={false} scrollEnabled={false}
data={Array.from({ length: 6 })} data={items}
numColumns={3} numColumns={3}
contentContainerStyle={{ gap: 10 }} contentContainerStyle={{ gap: 10 }}
columnWrapperStyle={{ gap: 6 }} columnWrapperStyle={{ gap: 6 }}
renderItem={() => ( keyExtractor={(item) => item.id}
<View style={{ flex: 1 }}> renderItem={({ item }) => (
<Pressable
style={{ flex: 1 }}
onPress={() => navigate(Routes.MusicDetails, { projectId: item.id })}
>
<Image <Image
source={img.placeholder3} source={item?.coverUrl ? { uri: item.coverUrl } : img.placeholder3}
style={{ width: "100%", height: 172, borderRadius: 10 }} style={{ width: "100%", height: 172, borderRadius: 10 }}
/> />
</View> </Pressable>
)} )}
/> />
</CardContainer> </CardContainer>
+28 -5
View File
@@ -12,8 +12,11 @@ import { icons, img } from "../../../assets";
import { size } from "../../../styles/Style"; import { size } from "../../../styles/Style";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { FONT_FAMILY } from "../../../styles/Fonts"; import { FONT_FAMILY } from "../../../styles/Fonts";
import { useGlobal } from "reactn";
import { projectsRef, arrayUnion, arrayRemove } from "../../../config/firebase";
const MusicCard = ({ onPress, onPressMore }) => { const MusicCard = ({ onPress, onPressMore, title = "Sans titre", subtitle = "MusicLand", imageUri = null, projectId = null, likedBy = [] }) => {
const [currentUID] = useGlobal("currentUID");
const [selected, setSelected] = useState(false); const [selected, setSelected] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [layout, setLayout] = useState(null); const [layout, setLayout] = useState(null);
@@ -26,6 +29,26 @@ const MusicCard = ({ onPress, onPressMore }) => {
} }
}, [layout]); }, [layout]);
useEffect(() => {
if (Array.isArray(likedBy) && currentUID) {
setSelected(likedBy.includes(currentUID));
}
}, [JSON.stringify(likedBy), currentUID]);
const toggleLike = async () => {
if (!projectId || !currentUID) return;
const next = !selected;
setSelected(next);
try {
await projectsRef.doc(projectId).update({
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
});
} catch (e) {
// rollback on failure
setSelected(!next);
}
};
const onPressMenu = () => { const onPressMenu = () => {
onPressMore?.(menuPos); onPressMore?.(menuPos);
}; };
@@ -41,7 +64,7 @@ const MusicCard = ({ onPress, onPressMore }) => {
onLayout={(e) => setLayout(e.nativeEvent.layout)} onLayout={(e) => setLayout(e.nativeEvent.layout)}
> >
<Image <Image
source={img.placeholder2} source={imageUri ? { uri: imageUri } : img.placeholder2}
style={{ ...size({ size: 60 }), borderRadius: 12 }} style={{ ...size({ size: 60 }), borderRadius: 12 }}
/> />
<View style={styles.blurContainer}> <View style={styles.blurContainer}>
@@ -57,8 +80,8 @@ const MusicCard = ({ onPress, onPressMore }) => {
} }
> >
<View> <View>
<Text style={styles.title}>Alors on danse</Text> <Text style={styles.title}>{title || "Sans titre"}</Text>
<Text style={styles.subTitle}>Stromae</Text> <Text style={styles.subTitle}>{subtitle || "MusicLand"}</Text>
</View> </View>
<View <View
style={{ style={{
@@ -66,7 +89,7 @@ const MusicCard = ({ onPress, onPressMore }) => {
gap: 12, gap: 12,
}} }}
> >
<Pressable onPress={() => setSelected(!selected)}> <Pressable onPress={toggleLike}>
<Image <Image
source={selected ? icons.heart : icons.heartOutline} source={selected ? icons.heart : icons.heartOutline}
style={size({ size: 24 })} style={size({ size: 24 })}
+18 -3
View File
@@ -4,17 +4,32 @@ import CardContainer from "./CardContainer";
import MusicCard from "./MusicCard"; import MusicCard from "./MusicCard";
import { navigate } from "../../../navigation/NavigationService"; import { navigate } from "../../../navigation/NavigationService";
import { Routes } from "../../../navigation"; import { Routes } from "../../../navigation";
import { useUser } from "../../../providers/UserDataProvider";
const MyMusic = () => { const MyMusic = () => {
const { userProjects = [] } = useUser();
const projects = Array.isArray(userProjects)
? userProjects.slice(0, 3)
: [];
return ( return (
<CardContainer <CardContainer
label="Mes musiques" label="Mes musiques"
onPress={() => navigate(Routes.AllMyMusic)} onPress={() => navigate(Routes.AllMyMusic)}
> >
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
{Array.from({ length: 3 }).map((_, index) => ( {Array.isArray(projects) &&
<MusicCard key={index} /> projects.map((p) => (
))} <MusicCard
key={p.id}
title={p?.title}
subtitle={"MusicLand"}
imageUri={p?.coverUrl || null}
projectId={p?.id}
likedBy={p?.likedBy || []}
onPress={() => navigate(Routes.MusicDetails, { projectId: p.id })}
/>
))}
</View> </View>
</CardContainer> </CardContainer>
); );
+48 -24
View File
@@ -1,23 +1,50 @@
/* eslint-disable react/display-name */ import React, { useState } from "react";
import React from "reactn"; import { useGlobal } from "reactn";
import { Pressable, Text, View } from "react-native"; import { Pressable, Text, View } from "react-native";
import Page from "../layouts/Page.js"; import Page from "../layouts/Page.js";
import { background } from "../assets/index.js"; import { background } from "../assets";
import { Input } from "../components/Input.js"; import { Input } from "../components/Input.js";
import GradientButton from "../components/GradientButton.js"; import GradientButton from "../components/GradientButton.js";
import Palette from "../styles/Palette.js"; import Palette from "../styles/Palette.js";
import { FONT_FAMILY } from "../styles/Fonts.js"; import { FONT_FAMILY } from "../styles/Fonts.js";
import { navigate } from "../navigation/NavigationService.js"; import { navigate } from "../navigation/NavigationService.js";
import { Routes } from "../navigation/Routes.js"; import { Routes } from "../navigation";
import ItemContainer from "../components/ItemContainer/ItemContainer.js"; import ItemContainer from "../components/ItemContainer/ItemContainer.js";
import firebase, { usersRef } from "../config/firebase";
export default ({ navigation }) => { export default ({ navigation }) => {
const [, setTooltip] = useGlobal("_tooltip");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const onLogin = async () => {
try {
setLoading(true);
await firebase.auth().signInWithEmailAndPassword(email.trim(), password);
const uid = firebase.auth().currentUser?.uid;
if (!uid) throw new Error("Aucun utilisateur après connexion");
const snap = await usersRef.doc(uid).get();
const hasUserName = !!snap.data()?.userName;
if (hasUserName) {
navigation.reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
} else {
navigation.reset({ index: 0, routes: [{ name: Routes.CreatePseudo }] });
}
} catch (e) {
console.log("Login error", e?.message);
setTooltip({ text: e?.message || "Connexion impossible", type: "error" });
} finally {
setLoading(false);
}
};
return ( return (
<Page <Page
backgroundImg={background.homeBG} backgroundImg={background.homeBG}
headerType="NAVIGATION" headerType="NAVIGATION"
title="Connexion" title="Connexion"
hideBackButton
> >
<View style={{ flex: 1, paddingTop: 20 }}> <View style={{ flex: 1, paddingTop: 20 }}>
<ItemContainer height={490}> <ItemContainer height={490}>
@@ -30,7 +57,7 @@ export default ({ navigation }) => {
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}} }}
> >
Bonjour, pseudo! Bonjour !
</Text> </Text>
<Text <Text
style={{ style={{
@@ -45,8 +72,11 @@ export default ({ navigation }) => {
</View> </View>
<View style={{ gap: 20 }}> <View style={{ gap: 20 }}>
<Input <Input
placeholder="Adresse mail ou pseudo" placeholder="Adresse mail"
label="Adresse mail ou pseudo" label="Adresse mail"
type="email"
value={email}
setValue={setEmail}
isBlur isBlur
/> />
<View style={{ gap: 4 }}> <View style={{ gap: 4 }}>
@@ -54,9 +84,13 @@ export default ({ navigation }) => {
placeholder="Mot de passe" placeholder="Mot de passe"
label="Mot de passe" label="Mot de passe"
type="password" type="password"
value={password}
setValue={setPassword}
isBlur isBlur
/> />
<Pressable> <Pressable
onPress={() => navigate(Routes.ForgotPassword)}
>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
@@ -70,12 +104,13 @@ export default ({ navigation }) => {
</View> </View>
</View> </View>
<GradientButton <GradientButton
title="Poursuivre la création" title="Se connecter"
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={() => navigate(Routes.BottomTab)} onPress={onLogin}
disabled={loading || !email || !password}
/> />
<View <View
style={{ style={{
@@ -83,17 +118,6 @@ export default ({ navigation }) => {
gap: 4, gap: 4,
}} }}
> >
<Text
style={{
fontSize: 12,
color: Palette.gray,
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
}}
>
En tinscrivant, tu acceptes nos conditions générales{"\n"}
dutilisation et notre politique de confidentialité.
</Text>
<Pressable onPress={() => navigate(Routes.Register)}> <Pressable onPress={() => navigate(Routes.Register)}>
<Text <Text
style={{ style={{
@@ -102,13 +126,13 @@ export default ({ navigation }) => {
fontFamily: FONT_FAMILY.HelveticaNeueRegular, fontFamily: FONT_FAMILY.HelveticaNeueRegular,
}} }}
> >
Tu as déjà un compte?{" "} Pas encore de compte?{" "}
<Text <Text
style={{ style={{
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}} }}
> >
Se connecter Créer un compte
</Text> </Text>
</Text> </Text>
</Pressable> </Pressable>
+110 -3
View File
@@ -1,5 +1,5 @@
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import React from "react"; import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { background } from "../../assets"; import { background } from "../../assets";
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from "../../styles";
@@ -7,8 +7,81 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import EditInput from "./components/EditInput"; import EditInput from "./components/EditInput";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { useGlobal } from "reactn";
import firebase, { usersRef } from "../../config/firebase";
import { goBack } from "../../navigation/NavigationService";
import { checkIfEmailIsValid } from "../../actions/signupActions";
import { Text } from "react-native";
import { FONT_FAMILY } from "../../styles/Fonts";
const ChangeEmailAddress = () => { const ChangeEmailAddress = () => {
const [email, setEmail] = useState("");
const [originalEmail, setOriginalEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [, setTooltip] = useGlobal("_tooltip");
const [currentUserData] = useGlobal("currentUserData");
const [loading, setLoading] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
useEffect(() => {
const authEmail = firebase.auth().currentUser?.email || "";
const profileEmail = (currentUserData?.email || authEmail || "").trim();
setEmail((prev) => (prev ? prev : profileEmail));
setOriginalEmail(profileEmail);
}, [currentUserData?.email]);
const onChangeEmail = (val) => {
setEmail(val);
const trimmed = (val || "").trim();
if (trimmed.length === 0) {
setEmailError("");
return;
}
if (!checkIfEmailIsValid({ email: trimmed })) {
setEmailError("Adresse email invalide");
} else {
setEmailError("");
}
};
const onSave = async () => {
const val = (email || "").trim();
if (!val) return;
try {
setLoading(true);
const user = firebase.auth().currentUser;
const uid = user?.uid;
if (!user || !uid) return;
// Reauthenticate user with current password before sensitive update
try {
const credential = firebase.auth.EmailAuthProvider.credential(
user.email,
currentPassword,
);
await user.reauthenticateWithCredential(credential);
} catch (reauthErr) {
throw {
code: "auth/wrong-password",
message: "Mot de passe incorrect.",
};
}
await user.updateEmail(val);
await usersRef.doc(uid).set({ email: val }, { merge: true });
setTooltip({ type: "success", text: "Email mis à jour" });
goBack();
} catch (e) {
console.log("ChangeEmailAddress error", e?.message);
const msg =
e?.code === "auth/requires-recent-login"
? "Veuillez vous reconnecter pour changer l'adresse email"
: e?.message || "Mise à jour impossible";
setTooltip({ type: "error", text: msg });
} finally {
setLoading(false);
}
};
return ( return (
<Page <Page
headerType="NAVIGATE" headerType="NAVIGATE"
@@ -35,15 +108,49 @@ const ChangeEmailAddress = () => {
Platform.OS !== "ios" ? "dimezisBlurView" : "none" Platform.OS !== "ios" ? "dimezisBlurView" : "none"
} }
> >
<EditInput placeholder="Adresse mail" label="Adresse mail" /> <EditInput
placeholder="Adresse mail"
label="Adresse mail"
value={email}
setValue={onChangeEmail}
type="email-address"
/>
<View style={{ height: 10 }} />
<EditInput
placeholder="Mot de passe actuel"
label="Mot de passe actuel"
value={currentPassword}
setValue={setCurrentPassword}
type="password"
/>
{!!emailError && (
<Text
style={{
fontSize: 12,
color: Palette.red,
fontFamily: FONT_FAMILY.InterRegular,
marginTop: 6,
}}
>
{emailError}
</Text>
)}
</BlurView> </BlurView>
</View> </View>
<GradientButton <GradientButton
title="Enregistrer les modifications" title={loading ? "Chargement..." : "Enregistrer les modifications"}
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={onSave}
disabled={
loading ||
!email?.trim() ||
!!emailError ||
email.trim() === originalEmail.trim() ||
!currentPassword?.trim()
}
/> />
</Page> </Page>
); );
+49 -4
View File
@@ -1,5 +1,5 @@
import { View, Text, Pressable, Platform } from "react-native"; import { View, Text, Pressable, Platform } from "react-native";
import React from "react"; import React, { useState } from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { background } from "../../assets"; import { background } from "../../assets";
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from "../../styles";
@@ -8,8 +8,42 @@ import { BlurView } from "expo-blur";
import EditInput from "./components/EditInput"; import EditInput from "./components/EditInput";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { useGlobal } from "reactn";
import firebase from "../../config/firebase";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
const ChangePassword = () => { const ChangePassword = () => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [loading, setLoading] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const onSave = async () => {
const oldVal = (oldPassword || "").trim();
const newVal = (newPassword || "").trim();
if (!oldVal || !newVal) return;
try {
setLoading(true);
const user = firebase.auth().currentUser;
if (!user?.email) return;
const credential = firebase.auth.EmailAuthProvider.credential(
user.email,
oldVal,
);
await user.reauthenticateWithCredential(credential);
await user.updatePassword(newVal);
setTooltip({ type: "success", text: "Mot de passe mis à jour" });
navigate(Routes.Settings);
} catch (e) {
console.log("ChangePassword error", e?.message);
const msg = e?.message || "Mise à jour impossible";
setTooltip({ type: "error", text: msg });
} finally {
setLoading(false);
}
};
return ( return (
<Page <Page
headerType="NAVIGATE" headerType="NAVIGATE"
@@ -38,9 +72,18 @@ const ChangePassword = () => {
} }
> >
<EditInput <EditInput
placeholder="Mot de passe" placeholder="Mot de passe actuel"
label="Mot de passe" label="Mot de passe actuel"
type="password" type="password"
value={oldPassword}
setValue={setOldPassword}
/>
<EditInput
placeholder="Nouveau mot de passe"
label="Nouveau mot de passe"
type="password"
value={newPassword}
setValue={setNewPassword}
/> />
<Pressable> <Pressable>
<Text <Text
@@ -56,11 +99,13 @@ const ChangePassword = () => {
</BlurView> </BlurView>
</View> </View>
<GradientButton <GradientButton
title="Enregistrer les modifications" title={loading ? "Chargement..." : "Enregistrer les modifications"}
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={onSave}
disabled={loading || !oldPassword?.trim() || !newPassword?.trim()}
/> />
</Page> </Page>
); );
+58 -4
View File
@@ -1,5 +1,5 @@
import { View, Text, Image, TextInput, Platform } from "react-native"; import { View, Text, Image, TextInput, Platform } from "react-native";
import React from "react"; import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { background, img } from "../../assets"; import { background, img } from "../../assets";
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from "../../styles";
@@ -10,8 +10,61 @@ import GradientButton from "../../components/GradientButton";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import { goBack } from "../../navigation/NavigationService"; import { goBack } from "../../navigation/NavigationService";
import EditInput from "./components/EditInput"; import EditInput from "./components/EditInput";
import { useGlobal } from "reactn";
import firebase, { usersRef } from "../../config/firebase";
const EditProfile = () => { const EditProfile = () => {
const [currentUserData] = useGlobal("currentUserData");
const [, setTooltip] = useGlobal("_tooltip");
const [pseudo, setPseudo] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
if (currentUserData?.userName && !pseudo) {
setPseudo(currentUserData.userName);
}
}, [currentUserData?.userName]);
const onSave = async () => {
const raw = pseudo || "";
const val = raw.trim();
if (!val) return;
try {
setLoading(true);
const uid = firebase.auth().currentUser?.uid;
if (!uid) return;
// Check uniqueness (case-insensitive)
const lower = val.toLowerCase();
const snap = await usersRef
.where("userNameLower", "==", lower)
.limit(1)
.get();
if (!snap.empty && snap.docs[0].id !== uid) {
setTooltip({ text: "Ce pseudo est déjà pris", type: "error" });
return;
}
await usersRef.doc(uid).set(
{
userName: val,
userNameLower: lower,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
setTooltip({ text: "Pseudo mis à jour", type: "success" });
goBack();
} catch (e) {
console.log("EditProfile onSave error", e?.message);
setTooltip({ text: e?.message || "Mise à jour impossible", type: "error" });
} finally {
setLoading(false);
}
};
return ( return (
<Page <Page
headerType="NAVIGATE" headerType="NAVIGATE"
@@ -56,15 +109,16 @@ const EditProfile = () => {
Modifier la photo Modifier la photo
</Text> </Text>
</View> </View>
<EditInput label="Pseudo" placeholder="Pseudo" /> <EditInput label="Pseudo" placeholder="Pseudo" value={pseudo} setValue={setPseudo} />
<GradientButton <GradientButton
title="Enregistrer les modifications" title={loading ? "Chargement..." : "Enregistrer les modifications"}
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={goBack} onPress={onSave}
disabled={loading || !pseudo?.trim() || pseudo?.trim() === (currentUserData?.userName || "")}
/> />
</BlurView> </BlurView>
</View> </View>
+24 -2
View File
@@ -1,5 +1,5 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React from "reactn"; import React, { useEffect, useGlobal } from "reactn";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { gutters, Palette, Style } from "../../styles"; import { gutters, Palette, Style } from "../../styles";
import { background } from "../../assets"; import { background } from "../../assets";
@@ -8,11 +8,33 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import Switch from "../../components/Switch"; import Switch from "../../components/Switch";
import { useState } from "react"; import { useState } from "react";
import firebase, { usersRef } from "../../config/firebase";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
export default (props) => { export default (props) => {
const [currentUID] = useGlobal("currentUID");
const [currentUserData] = useGlobal("currentUserData");
const [, setTooltip] = useGlobal("_tooltip");
const [isSwitch, setIsSwitch] = useState(false); const [isSwitch, setIsSwitch] = useState(false);
useEffect(() => {
setIsSwitch(!!currentUserData?.notifications);
}, [currentUserData?.notifications]);
const onToggle = async (next) => {
try {
setIsSwitch(next);
const uid = currentUID || firebase.auth().currentUser?.uid;
if (!uid) return;
await usersRef.doc(uid).set({ notifications: next }, { merge: true });
setTooltip({ type: "success", text: "Préférence enregistrée" });
} catch (e) {
setIsSwitch(!next);
setTooltip({ type: "error", text: e?.message || "Enregistrement impossible" });
}
};
return ( return (
<Page <Page
headerType="NAVIGATE" headerType="NAVIGATE"
@@ -50,7 +72,7 @@ export default (props) => {
> >
Notifications Notifications
</Text> </Text>
<Switch value={isSwitch} setValue={setIsSwitch} /> <Switch value={isSwitch} setValue={onToggle} />
</View> </View>
<Text <Text
style={{ style={{
+119 -18
View File
@@ -15,15 +15,25 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
import Style, { gutters, size } from "../../styles/Style"; import Style, { gutters, size } from "../../styles/Style";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import GradientButton from "../../components/GradientButton";
import BorderGradient from "../../components/BorderGradient/BorderGradient"; import BorderGradient from "../../components/BorderGradient/BorderGradient";
import MusicCard from "../Library/components/MusicCard"; import MusicCard from "../Library/components/MusicCard";
import { useUser } from "../../providers/UserDataProvider";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import { navigate } from "../../navigation/NavigationService"; import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { useGlobal } from "reactn";
import Animated, { Easing, FadeIn, FadeOut } from "react-native-reanimated";
import alert from "../../components/Alert";
import { projectsRef } from "../../config/firebase";
const Profile = () => { const Profile = () => {
const [selected, setSelected] = useState("Chansons"); const [selected, setSelected] = useState("Chansons");
const [currentUserData] = useGlobal("currentUserData");
const { userProjects } = useUser();
const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const onPressMenu = (item) => { const onPressMenu = (item) => {
setSelected(item); setSelected(item);
@@ -74,31 +84,29 @@ const Profile = () => {
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}} }}
> >
elia.mrn {currentUserData?.userName}
</Text> </Text>
</View> </View>
<View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}> <View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}>
<View style={{ flex: 1, alignItems: "center" }}> <View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>0</Text> <Text style={styles.value}>
{currentUserData?.playlist?.length || 0}
</Text>
<Text style={styles.label}>playbacks</Text> <Text style={styles.label}>playbacks</Text>
</View> </View>
<View style={{ flex: 1, alignItems: "center" }}> <View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>880</Text> <Text style={styles.value}>
{currentUserData?.followBy?.length || 0}
</Text>
<Text style={styles.label}>abonnés</Text> <Text style={styles.label}>abonnés</Text>
</View> </View>
<View style={{ flex: 1, alignItems: "center" }}> <View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}>6</Text> <Text style={styles.value}>
{currentUserData?.following?.length || 0}
</Text>
<Text style={styles.label}>abonnements</Text> <Text style={styles.label}>abonnements</Text>
</View> </View>
</View> </View>
<GradientButton
title="Suivre"
containerStyle={{
width: "80%",
alignSelf: "center",
marginTop: 18,
}}
/>
</BlurView> </BlurView>
</View> </View>
<View <View
@@ -176,20 +184,113 @@ const Profile = () => {
{selected === "Chansons" && ( {selected === "Chansons" && (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<FlatList <FlatList
data={Array.from({ length: 3 })} data={Array.isArray(userProjects) ? userProjects : []}
contentContainerStyle={{ contentContainerStyle={{
gap: 10, gap: 10,
}} }}
renderItem={() => ( keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<MusicCard <MusicCard
title={item?.title || "Sans titre"}
subtitle={currentUserData?.userName || "MusicLand"}
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
onPress={() => onPress={() =>
navigate(Routes.MusicDetails, { navigate(Routes.MusicDetails, { projectId: item.id })
action: "userProfile",
})
} }
onPressMore={(posTop) => {
setSelectedProjectId(item.id);
if (showMenu) {
if (posTop === menuTop) {
setShowMenu(false);
} else {
setShowMenu(false);
setTimeout(() => {
setMenuTop(posTop);
setShowMenu(true);
}, 200);
}
} else {
setMenuTop(posTop);
setShowMenu(true);
}
}}
/> />
)} )}
/> />
{showMenu && (
<Animated.View
entering={FadeIn.duration(200).easing(Easing.ease)}
exiting={FadeOut.duration(200).easing(Easing.ease)}
style={{ position: "absolute", right: 0, top: menuTop, zIndex: 2 }}
>
<BlurView
intensity={20}
style={{
paddingHorizontal: 12,
paddingVertical: 10,
backgroundColor: Palette.glass,
borderRadius: 12,
overflow: "hidden",
gap: 10,
}}
experimentalBlurMethod={
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
<Pressable onPress={() => setShowMenu(false)}>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Annuler
</Text>
</Pressable>
<Pressable
onPress={() => {
setShowMenu(false);
alert(
"Confirmer la suppression",
"Cette action supprimera définitivement ce projet.",
[
{ text: "Annuler", style: "cancel", onPress: () => {} },
{
text: "Supprimer",
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 },
);
}}
>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Supprimer
</Text>
</Pressable>
</BlurView>
</Animated.View>
)}
</View> </View>
)} )}
</View> </View>
+14 -5
View File
@@ -1,4 +1,4 @@
import { View, Text } from "react-native"; import { View } from "react-native";
import React from "react"; import React from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { background } from "../../assets"; import { background } from "../../assets";
@@ -9,6 +9,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import { navigate } from "../../navigation/NavigationService"; import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { useUserData } from "../../providers/UserDataProvider";
const SETTINGS = [ const SETTINGS = [
{ {
@@ -42,6 +43,17 @@ const SETTINGS = [
]; ];
const Settings = () => { const Settings = () => {
const { onSignOut } = useUserData();
const handleSignOut = async () => {
try {
await onSignOut();
} catch (e) {
console.log("Sign out error", e?.message);
} finally {
navigate(Routes.Login);
}
};
return ( return (
<Page <Page
headerType="NAVIGATION" headerType="NAVIGATION"
@@ -65,10 +77,7 @@ const Settings = () => {
))} ))}
</View> </View>
<View style={{ width: "80%", gap: 5, alignSelf: "center" }}> <View style={{ width: "80%", gap: 5, alignSelf: "center" }}>
<BorderGradientButton <BorderGradientButton title="Déconnexion" onPress={handleSignOut} />
title="Déconnexion"
onPress={() => navigate(Routes.Login)}
/>
<BorderGradientButton <BorderGradientButton
title="Supprimer mon profil" title="Supprimer mon profil"
titleStyle={{ titleStyle={{

Some files were not shown because too many files have changed in this diff Show More