+
+
+
${greeting}
Bienvenue sur MusicLand
diff --git a/functions/src/notifications.js b/functions/src/notifications.js
index a31c072..63b0280 100644
--- a/functions/src/notifications.js
+++ b/functions/src/notifications.js
@@ -27,6 +27,7 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
receiverCollection = "users",
message = "",
data: notifData = {},
+ mailOnly = false,
} = event.data.data();
if (!receiver || !message) {
@@ -40,27 +41,29 @@ exports.sendNotificationWhenDocIsCreated = onDocumentCreated(
emailNotifications = false,
} = (await db.collection(receiverCollection).doc(receiver).get()).data();
- const tokensSet = new Set(
- []
- .concat(Array.isArray(pushTokens) ? pushTokens : [])
- .concat(pushToken ? [pushToken] : [])
- .filter(Boolean),
- );
- const tokens = Array.from(tokensSet);
+ if (!mailOnly) {
+ const tokensSet = new Set(
+ []
+ .concat(Array.isArray(pushTokens) ? pushTokens : [])
+ .concat(pushToken ? [pushToken] : [])
+ .filter(Boolean),
+ );
+ const tokens = Array.from(tokensSet);
- if (!tokens?.length) {
- console.warn("User push token not found");
- return null;
+ if (!tokens?.length) {
+ console.warn("User push token not found");
+ return null;
+ }
+
+ await sendExpoNotification({
+ tokens,
+ receiverId: receiver,
+ receiverCollection,
+ title: title || "MusicLand",
+ message: message,
+ data: notifData || {},
+ });
}
-
- await sendExpoNotification({
- tokens,
- receiverId: receiver,
- receiverCollection,
- title: title || "MusicLand",
- message: message,
- data: notifData || {},
- });
if (emailNotifications && !!email) {
try {
if (!email) {
@@ -219,6 +222,7 @@ const sendNotification = async ({
receiverCollection = "users",
title = "",
message = null,
+ mailOnly = false,
data = {},
}) => {
try {
@@ -234,6 +238,7 @@ const sendNotification = async ({
time: admin.firestore.FieldValue.serverTimestamp(),
read: false,
readAt: null,
+ mailOnly,
data,
};
const { id } = await refList.notifications.add(payload);
diff --git a/functions/src/users.js b/functions/src/users.js
index 7c87ac9..e0bba5d 100644
--- a/functions/src/users.js
+++ b/functions/src/users.js
@@ -23,12 +23,18 @@ exports.onUserCreated = onDocumentCreated("users/{userID}", async (event) => {
return;
}
try {
- await resendClient.emails.send({
+ console.log(`Sending welcome email to ${email}`);
+ const { data, error } = await resendClient.emails.send({
from: WELCOME_EMAIL_FROM,
to: [email],
subject: WELCOME_EMAIL_SUBJECT,
html: welcomeTemplate({ firstName, lastName }),
});
+ if (error) {
+ console.log("Failed to send welcome email:", error);
+ return;
+ }
+ console.log("Welcome email sent:", data);
} catch (error) {
console.log("Failed to send welcome email:", error);
}
diff --git a/src/assets/comments.png b/src/assets/comments.png
deleted file mode 100644
index ebd856b..0000000
Binary files a/src/assets/comments.png and /dev/null differ
diff --git a/src/components/AppActionSheet.js b/src/components/AppActionSheet.js
index 98312c2..32e438e 100644
--- a/src/components/AppActionSheet.js
+++ b/src/components/AppActionSheet.js
@@ -1,26 +1,38 @@
import { Portal } from "@gorhom/portal";
import { BlurView } from "expo-blur";
-import React from "react";
+import React, { useCallback } from "react";
import { Platform, Pressable, StyleSheet, View } from "react-native";
-import ActionSheet from "react-native-actions-sheet";
+import ActionSheet, { SheetManager } from "react-native-actions-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { isWeb } from "../hooks/useLayoutType";
import { gutters, Palette } from "../styles";
const AppActionSheet = ({
+ id,
children,
webModal = false,
onClose = () => {},
...sheetProps
}) => {
const insets = useSafeAreaInsets();
+ const handleRequestClose = useCallback(() => {
+ if (id) {
+ Promise.resolve(SheetManager.hide(id))
+ .catch(() => {})
+ .finally(() => {
+ onClose?.();
+ });
+ return;
+ }
+ onClose?.();
+ }, [id, onClose]);
if (isWeb && webModal) {
return (
@@ -36,6 +48,7 @@ const AppActionSheet = ({
return (
{
+ const resolvedSize = HEIGHT_BY_SIZE[size] ? size : "medium";
+ const buttonHeight = HEIGHT_BY_SIZE[resolvedSize];
+ const fontSize = FONT_SIZE_BY_SIZE[resolvedSize];
+ const iconSize = resolvedSize === "small" ? 14 : 16;
+
return (
- {icon && }
+ {icon && }
{
+ const resolvedSize = HEIGHT_BY_SIZE[size] ? size : "medium";
+ const buttonHeight = HEIGHT_BY_SIZE[resolvedSize];
+ const fontSize = FONT_SIZE_BY_SIZE[resolvedSize];
+
return (
}
{title}
diff --git a/src/components/ListSelection/ListSelection.js b/src/components/ListSelection/ListSelection.js
index 55fb6c0..e3425bc 100644
--- a/src/components/ListSelection/ListSelection.js
+++ b/src/components/ListSelection/ListSelection.js
@@ -166,6 +166,7 @@ const ListSelection = ({
const itemKey = buildItemKey(String(val ?? ""), cat);
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
const showHoverOutline = isHovered && !sel;
+ const showSelectionOutline = !isWeb && sel;
const baseContent = renderSimpleContent(item);
const headerContainerStyle = {
@@ -205,7 +206,7 @@ const ListSelection = ({
{withWebBlueGradientIfSelected(baseContent, sel)}
- {showHoverOutline && (
+ {(showHoverOutline || showSelectionOutline) && (
{
if (typeof onPress === "function") {
onPress();
@@ -41,7 +45,7 @@ export default function ShareBtn({ style, onPress = null }) {
marginRight: 5,
}}
>
- {"Partager l’expérience"}
+ {label}
diff --git a/src/components/modal/DeleteAccountModal.js b/src/components/modal/DeleteAccountModal.js
index 611d34c..f0f1f08 100644
--- a/src/components/modal/DeleteAccountModal.js
+++ b/src/components/modal/DeleteAccountModal.js
@@ -30,7 +30,7 @@ const DeleteAccountModal = () => {
onPress: () => {
alert(
"Confirmation",
- "Dernière vérification: supprimer définitivement votre profil ?",
+ "Dernière vérification : souhaitez-vous supprimer définitivement votre profil ?",
[
{ text: "Non", style: "cancel", onPress: () => {} },
{
@@ -85,8 +85,8 @@ const DeleteAccountModal = () => {
textAlign: "center",
}}
>
- Es-tu sur de vouloir supprimer ton compte?{"\n"}Attention, cette
- action est irréversible!
+ Es-tu sûr de vouloir supprimer ton compte ?{"\n"}Attention, cette
+ action est irréversible !
diff --git a/src/components/modal/ReportModal.js b/src/components/modal/ReportModal.js
new file mode 100644
index 0000000..91c0ab5
--- /dev/null
+++ b/src/components/modal/ReportModal.js
@@ -0,0 +1,268 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { SheetManager } from "react-native-actions-sheet";
+import {
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import { useGlobal } from "reactn";
+import BorderGradientButton from "../BorderGradientButton";
+import GradientButton from "../GradientButton";
+import { REPORT_OPTIONS } from "../../constants/reportOptions";
+import { reportsRef, serverTimestamp } from "../../config/firebase";
+import { Palette } from "../../styles";
+import { FONT_FAMILY } from "../../styles/Fonts";
+
+const SHEET_ID = "Report";
+
+const ReportModal = ({ payload }) => {
+ const [, setTooltip] = useGlobal("_tooltip");
+ const [selectedReason, setSelectedReason] = useState(REPORT_OPTIONS[0].key);
+ const [details, setDetails] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isVisible, setIsVisible] = useState(true);
+
+ const resetForm = useCallback(() => {
+ setSelectedReason(REPORT_OPTIONS[0].key);
+ setDetails("");
+ setIsSubmitting(false);
+ }, []);
+
+ const targetContext = useMemo(() => {
+ return {
+ type: payload?.targetType || "music",
+ projectId: payload?.projectId || null,
+ playbackId: payload?.playbackId || null,
+ title: payload?.title || "",
+ ownerId: payload?.ownerId || null,
+ };
+ }, [payload]);
+
+ const closeSheet = useCallback(() => {
+ setIsVisible(false);
+ Promise.resolve(SheetManager.hide(SHEET_ID))
+ .catch(() => {})
+ .finally(() => {
+ resetForm();
+ });
+ }, [resetForm]);
+
+ useEffect(() => {
+ setIsVisible(true);
+ }, [payload]);
+
+ const handleSubmit = useCallback(async () => {
+ if (isSubmitting) return;
+ if (selectedReason === "other" && !details.trim()) return;
+ try {
+ setIsSubmitting(true);
+ const selectedOption = REPORT_OPTIONS.find(
+ (option) => option.key === selectedReason,
+ );
+ await reportsRef.add({
+ ...targetContext,
+ reason: selectedReason,
+ reasonLabel: selectedOption?.label || selectedReason,
+ details: details.trim() || null,
+ platform: Platform.OS,
+ createdAt: serverTimestamp(),
+ });
+ setTooltip({
+ type: "success",
+ text: "Merci pour ton signalement, notre équipe va l'examiner.",
+ });
+ closeSheet();
+ } catch (error) {
+ setTooltip({
+ type: "error",
+ text:
+ error?.message ||
+ "Impossible d'envoyer le signalement pour le moment.",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ }, [
+ details,
+ closeSheet,
+ isSubmitting,
+ selectedReason,
+ setTooltip,
+ targetContext,
+ ]);
+
+ const showDetailsField = selectedReason === "other";
+
+ return (
+
+
+
+
+
+
+ Signaler ce contenu
+ {targetContext?.title ? (
+ {targetContext.title}
+ ) : null}
+
+
+
+ {REPORT_OPTIONS.map((option) => {
+ const isSelected = option.key === selectedReason;
+ return (
+ setSelectedReason(option.key)}
+ >
+
+ {option.label}
+
+ );
+ })}
+
+
+ {showDetailsField ? (
+
+ Détails (optionnel)
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
+ justifyContent: "center",
+ paddingHorizontal: 24,
+ position: "relative",
+ },
+ backdrop: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ modalWrapper: {
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ card: {
+ gap: 20,
+ borderRadius: 24,
+ padding: 24,
+ backgroundColor: "rgba(18, 16, 24, 0.94)",
+ width: "100%",
+ maxWidth: 420,
+ alignSelf: "center",
+ },
+ heading: {
+ fontSize: 20,
+ color: Palette.white,
+ fontFamily: FONT_FAMILY.HelveticaNeueMedium,
+ textAlign: "center",
+ },
+ subheading: {
+ fontSize: 16,
+ color: Palette.transparentWhite,
+ fontFamily: FONT_FAMILY.InterMedium,
+ textAlign: "center",
+ },
+ optionRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ borderRadius: 12,
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
+ gap: 12,
+ },
+ optionRowSelected: {
+ backgroundColor: "rgba(251, 104, 168, 0.18)",
+ },
+ radio: {
+ width: 16,
+ height: 16,
+ borderRadius: 8,
+ borderWidth: 2,
+ borderColor: Palette.transparentWhite,
+ },
+ radioSelected: {
+ borderColor: Palette.primary,
+ backgroundColor: Palette.primary,
+ },
+ optionLabel: {
+ flex: 1,
+ fontSize: 15,
+ color: Palette.white,
+ fontFamily: FONT_FAMILY.InterRegular,
+ },
+ fieldLabel: {
+ fontSize: 14,
+ color: Palette.transparentWhite,
+ fontFamily: FONT_FAMILY.InterMedium,
+ },
+ textArea: {
+ minHeight: 90,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.12)",
+ padding: 12,
+ color: Palette.white,
+ fontFamily: FONT_FAMILY.InterRegular,
+ fontSize: 14,
+ textAlignVertical: "top",
+ backgroundColor: "rgba(0, 0, 0, 0.25)",
+ },
+});
+
+export default ReportModal;
diff --git a/src/config/firebase.js b/src/config/firebase.js
index f8e86ac..139abaf 100644
--- a/src/config/firebase.js
+++ b/src/config/firebase.js
@@ -52,6 +52,7 @@ export const playlistsRef = firestore.collection("playlists");
export const chatsRef = firestore.collection("chats");
export const videosRef = firestore.collection("videos");
export const notificationsRef = firestore.collection("notifications");
+export const reportsRef = firestore.collection("reports");
export const { arrayUnion, arrayRemove, increment, serverTimestamp } =
firebase.firestore.FieldValue;
diff --git a/src/config/initialGlobalState.js b/src/config/initialGlobalState.js
index 66d499d..d9361d7 100644
--- a/src/config/initialGlobalState.js
+++ b/src/config/initialGlobalState.js
@@ -1,4 +1,5 @@
import { Palette } from "../styles";
+import { musiclandProductionBaseUrl } from "../data";
export default {
currentUID: null,
@@ -16,6 +17,7 @@ export default {
url: null,
title: "",
},
+ productionBaseUrl: musiclandProductionBaseUrl,
_isLoading: false,
_loadingMessage: null,
diff --git a/src/constants/reportOptions.js b/src/constants/reportOptions.js
new file mode 100644
index 0000000..0fe27d9
--- /dev/null
+++ b/src/constants/reportOptions.js
@@ -0,0 +1,18 @@
+export const REPORT_OPTIONS = [
+ {
+ key: "copyright",
+ label: "Ce morceau enfreint mes droits d'auteur",
+ },
+ {
+ key: "problematic_content",
+ label: "Le contenu de ce morceau est problématique",
+ },
+ {
+ key: "spam",
+ label: "Il s'agit de spam ou de contenu promotionnel non sollicité",
+ },
+ {
+ key: "other",
+ label: "Autre…",
+ },
+];
diff --git a/src/constants/strings.js b/src/constants/strings.js
new file mode 100644
index 0000000..02c3c57
--- /dev/null
+++ b/src/constants/strings.js
@@ -0,0 +1,35 @@
+export const strings = {
+ writing: {
+ steps: {
+ contextTitle: "Étape 2: Quel est le contexte ?",
+ contextSubtitle:
+ "Ajoute quelques détails pour nous guider (ex.: anniversaire, équipe de sport ou d’entreprise…)",
+ contextExamples:
+ "Exemples : « Anniversaire de Clara – ton joyeux et pop », « Hymne d’équipe – style rap énergique », « Esprit d’entreprise – ambiance motivante »",
+ emotionTitle: "Étape 3: Quelle émotion veux-tu transmettre ?",
+ emotionSubtitle: "Sélectionne une seule intention émotionnelle.",
+ },
+ labels: {
+ otherObjective: "As-tu un autre objectif ?",
+ otherObjectivePlaceholder: "Décrire l’objectif",
+ },
+ onboarding: {
+ heroTitle: "Bienvenue dans ton atelier d'écriture",
+ heroSubtitle:
+ "C'est ici qu'ensemble nous allons écrire ta chanson, alors vas-y, suis le Guide et Étape 1 Quel est ton objectif?",
+ primaryCta: "Je crée mes paroles",
+ secondaryCta: "J’ai déjà mes paroles",
+ secondaryNote:
+ "*Si tu as déjà des paroles tu auras l’occasion de les intégrer le moment venu*",
+ },
+ lyrics: {
+ title: "Proposition de paroles",
+ instructions:
+ "Relis et personnalise les paroles proposées. Tu peux soit personnaliser la version actuelle, soit lancer une nouvelle génération.",
+ personalizationBanner:
+ "Personnalisation : tu peux modifier chaque section et si tu as tes paroles intègre les à la structure proposée",
+ },
+ },
+};
+
+export default strings;
diff --git a/src/data/index.js b/src/data/index.js
index d351ef6..533848a 100644
--- a/src/data/index.js
+++ b/src/data/index.js
@@ -13,7 +13,9 @@ export const firebaseDashboardUrl =
export const appleAppStoreUrl = "https://apps.apple.com/app";
export const algoliaAppUrl = "https://dashboard.algolia.com/apps";
-export const musiclandShareUrl = "https://musicland.ai";
+export const musiclandProductionBaseUrl =
+ "https://musicland-one.vercel.app";
+export const musiclandShareUrl = musiclandProductionBaseUrl;
export const musiclandShareHeading = "Partagez l'expérience MusicLand";
export const musiclandShareMessage =
"Invite tes proches a decouvrir MusicLand et cree des experiences musicales ensemble.";
diff --git a/src/providers/PlayerProvider.js b/src/providers/PlayerProvider.js
index 8d802e2..401f568 100644
--- a/src/providers/PlayerProvider.js
+++ b/src/providers/PlayerProvider.js
@@ -54,6 +54,8 @@ const HIDDEN_ROUTE_NAMES = new Set([
Routes.ChangePassword,
Routes.Notifications,
Routes.Language,
+ Routes.Login,
+ Routes.Register,
]);
const noopAsync = async () => {};
diff --git a/src/providers/UniversalLinkProvider.js b/src/providers/UniversalLinkProvider.js
index 73c13fc..49cc68d 100644
--- a/src/providers/UniversalLinkProvider.js
+++ b/src/providers/UniversalLinkProvider.js
@@ -2,7 +2,8 @@ import { useContext, useState, useEffect } from "reactn";
import * as Linking from "expo-linking";
import { UserDataContext } from "./UserDataProvider";
import { isWeb } from "../hooks/useLayoutType";
-import { navigateToTask } from "../navigation/NavigationService";
+import { navigate, navigateToTask } from "../navigation/NavigationService";
+import { Routes } from "../navigation/Routes";
import { SplashAnimationContext } from "./SplashAnimationProvider";
const appJson = require("../../app.json");
@@ -16,6 +17,7 @@ const UniversalLinkProvider = ({ children }) => {
const { isFullyLoaded } = useContext(SplashAnimationContext);
const [tempTaskData, setTempTaskData] = useState(null);
+ const [pendingMusicId, setPendingMusicId] = useState(null);
useEffect(() => {
const handleDeepLink = async (event) => {
@@ -51,13 +53,13 @@ const UniversalLinkProvider = ({ children }) => {
}
} catch (error) {
console.error("Redirection error:", error);
- handleParams(path);
+ handleParams(path, queryParams);
}
} else {
- handleParams(path);
+ handleParams(path, queryParams);
}
} else {
- handleParams(path);
+ handleParams(path, queryParams);
}
};
@@ -86,8 +88,31 @@ const UniversalLinkProvider = ({ children }) => {
}
}, [tempTaskData, currentUID, isFullyLoaded]);
- const handleParams = (path) => {
- // console.log("path", path);
+ useEffect(() => {
+ if (!pendingMusicId || !isFullyLoaded) {
+ return;
+ }
+
+ navigate(Routes.MusicDetails, {
+ projectId: pendingMusicId,
+ autoPlay: true,
+ action: "share",
+ });
+ setPendingMusicId(null);
+ }, [isFullyLoaded, pendingMusicId]);
+
+ const handleParams = (path, _queryParams = {}) => {
+ if (typeof path === "string" && path.length > 0) {
+ const normalized = path.replace(/^\/+/, "");
+ const [first, second] = normalized.split("/");
+ if (first?.toLowerCase() === "music" && second) {
+ try {
+ setPendingMusicId(decodeURIComponent(second));
+ } catch (_) {
+ setPendingMusicId(second);
+ }
+ }
+ }
cleanURL();
};
diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js
index 34ee177..9e712e3 100644
--- a/src/screens/Library/MusicDetails.js
+++ b/src/screens/Library/MusicDetails.js
@@ -17,6 +17,7 @@ import {
import { useGlobal } from "reactn";
import { background, icons } from "../../assets";
import PressableScale from "../../components/PressableScale";
+import ShareBtn from "../../components/ShareBtn/ShareBtn";
import Slider from "../../components/Slider";
import {
arrayRemove,
@@ -37,6 +38,11 @@ import {
getSegmentMeta,
normalizeStructureType,
} from "../../utils/songStructure";
+import {
+ createMusicSharePayload,
+ openShareSheet,
+} from "../../utils/shareSheet";
+import { Feather } from "@expo/vector-icons";
// 20 secondes
const timeBeforeIncrement = 20000;
@@ -44,6 +50,7 @@ const timeBeforeIncrement = 20000;
const MusicDetails = ({ route }) => {
const { params } = route || {};
const action = params?.action;
+ const autoPlayRequested = params?.autoPlay;
const projectId = params?.projectId || null;
const [fav, setFav] = useState(false);
const [currentUID] = useGlobal("currentUID");
@@ -51,6 +58,7 @@ const MusicDetails = ({ route }) => {
const listenedMsRef = useRef(0);
const incrementDoneRef = useRef(false);
const timerRef = useRef(null);
+ const hasAutoPlayedRef = useRef(false);
const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null,
@@ -105,6 +113,33 @@ const MusicDetails = ({ route }) => {
};
}, [trackId, songUrl, title, artist, coverUrl, projectId]);
+ const sharePayload = useMemo(
+ () =>
+ createMusicSharePayload({
+ projectId,
+ title,
+ artist,
+ }),
+ [projectId, title, artist]
+ );
+
+ const handleShare = useCallback(() => {
+ if (!sharePayload) return;
+ openShareSheet(sharePayload);
+ }, [sharePayload]);
+
+ const handleReport = useCallback(() => {
+ if (!projectId) return;
+ SheetManager.show("Report", {
+ payload: {
+ targetType: "music",
+ projectId,
+ title,
+ ownerId: project?.userId || owner?.id || null,
+ },
+ });
+ }, [owner?.id, project?.userId, projectId, title]);
+
const {
isCurrent: isCurrentTrack,
isPlaying: isTrackPlaying,
@@ -137,6 +172,7 @@ const MusicDetails = ({ route }) => {
useEffect(() => {
listenedMsRef.current = 0;
incrementDoneRef.current = false;
+ hasAutoPlayedRef.current = false;
}, [trackId]);
// Start/stop a timer to accumulate listened milliseconds while playing
@@ -247,6 +283,34 @@ const MusicDetails = ({ route }) => {
[durationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo]
);
+ useEffect(() => {
+ if (!autoPlayRequested || hasAutoPlayedRef.current) return;
+ if (!trackDescriptor || !songUrl) return;
+
+ const run = async () => {
+ try {
+ if (!isCurrentTrack) {
+ await ensureLoaded({ startPositionMs: 0, autoPlay: true });
+ } else if (!isTrackPlaying) {
+ await resumeTrack();
+ }
+ hasAutoPlayedRef.current = true;
+ } catch (e) {
+ console.log("MusicDetails autoplay error", e?.message);
+ }
+ };
+
+ run();
+ }, [
+ autoPlayRequested,
+ trackDescriptor,
+ songUrl,
+ isCurrentTrack,
+ ensureLoaded,
+ isTrackPlaying,
+ resumeTrack,
+ ]);
+
const handleSliderSeekEnd = useCallback(async () => {
try {
if (wasPlayingBeforeSeek.current) {
@@ -627,17 +691,24 @@ const MusicDetails = ({ route }) => {
style={styles.img}
/>
)}
-
-
- {title}
- {artist}
-
-
- {
- if (!projectId || !currentUID) return;
- const next = !fav;
- setFav(next);
+
+
+
+ {title}
+ {artist}
+
+
+ {
+ if (!projectId || !currentUID) return;
+ const next = !fav;
+ setFav(next);
try {
await projectsRef.doc(projectId).update({
likedBy: next
@@ -655,6 +726,12 @@ const MusicDetails = ({ route }) => {
resizeMode="contain"
/>
+
+
+
+ {!!sharePayload && (
+
+ )}
SheetManager.show("Playlist", { payload: { projectId } })
@@ -666,6 +743,7 @@ const MusicDetails = ({ route }) => {
resizeMode="contain"
/>
+
diff --git a/src/screens/Library/MusicDetails.web.js b/src/screens/Library/MusicDetails.web.js
index 920017a..264f27b 100644
--- a/src/screens/Library/MusicDetails.web.js
+++ b/src/screens/Library/MusicDetails.web.js
@@ -24,6 +24,7 @@ import { useGlobal } from "reactn";
import { background, icons } from "../../assets";
import PressableScale from "../../components/PressableScale";
import Slider from "../../components/Slider";
+import Svg, { Circle } from "react-native-svg";
import {
arrayRemove,
arrayUnion,
@@ -45,6 +46,11 @@ import {
normalizeStructureType,
segmentRequiresLyrics,
} from "../../utils/songStructure";
+import {
+ createMusicSharePayload,
+ openShareSheet,
+} from "../../utils/shareSheet";
+import { Feather } from "@expo/vector-icons";
// 20 secondes
const timeBeforeIncrement = 20000;
@@ -52,14 +58,17 @@ const timeBeforeIncrement = 20000;
const MusicDetails = ({ route }) => {
const { params } = route || {};
const action = params?.action;
+ const autoPlayRequested = params?.autoPlay;
const projectId = params?.projectId || null;
const [fav, setFav] = useState(false);
const [currentUID] = useGlobal("currentUID");
+ const [, setTooltip] = useGlobal("_tooltip");
const wasPlayingBeforeSeek = React.useRef(false);
const lastSeekTargetMs = React.useRef(null);
const listenedMsRef = React.useRef(0);
const incrementDoneRef = React.useRef(false);
const timerRef = React.useRef(null);
+ const hasAutoPlayedRef = React.useRef(false);
const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null,
@@ -116,6 +125,108 @@ const MusicDetails = ({ route }) => {
};
}, [trackId, songUrl, title, artist, coverUrl, projectId]);
+ const sharePayload = useMemo(
+ () =>
+ createMusicSharePayload({
+ projectId,
+ title,
+ artist,
+ }),
+ [projectId, title, artist]
+ );
+
+ const handleShare = useCallback(() => {
+ if (!sharePayload) return;
+ openShareSheet(sharePayload);
+ }, [sharePayload]);
+
+ const [isDownloading, setIsDownloading] = useState(false);
+ const [downloadProgress, setDownloadProgress] = useState(0);
+
+ const handleDownload = useCallback(async () => {
+ if (!songUrl || isDownloading) return;
+ try {
+ setIsDownloading(true);
+ setDownloadProgress(0);
+
+ const response = await fetch(songUrl);
+ if (!response.ok) {
+ throw new Error(`download_failed_${response.status}`);
+ }
+
+ const contentType = response.headers.get("content-type") || "audio/mpeg";
+ const total = Number(response.headers.get("content-length")) || 0;
+ const sanitizedTitle = String(title || "musicland-track")
+ .replace(/[\\/:*?"<>|]+/g, "-")
+ .trim()
+ .slice(0, 80);
+ const fileName = `${sanitizedTitle || "musicland-track"}.mp3`;
+
+ if (response.body && typeof response.body.getReader === "function") {
+ const reader = response.body.getReader();
+ const chunks = [];
+ let received = 0;
+ let pseudoProgress = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ if (value) {
+ chunks.push(value);
+ received += value.length;
+ if (total > 0) {
+ setDownloadProgress(Math.min(1, received / total));
+ } else {
+ pseudoProgress = Math.min(0.95, pseudoProgress + 0.05);
+ setDownloadProgress(pseudoProgress);
+ }
+ }
+ }
+ const blob = new Blob(chunks, { type: contentType });
+ setDownloadProgress(1);
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ } else {
+ const blob = await response.blob();
+ setDownloadProgress(1);
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+ } catch (error) {
+ console.error("MusicDetails.download", error);
+ setTooltip({
+ type: "error",
+ text: "Téléchargement impossible",
+ });
+ } finally {
+ setIsDownloading(false);
+ setTimeout(() => setDownloadProgress(0), 400);
+ }
+ }, [isDownloading, setTooltip, songUrl, title]);
+
+ const handleReport = useCallback(() => {
+ if (!projectId) return;
+ SheetManager.show("Report", {
+ payload: {
+ targetType: "music",
+ projectId,
+ title,
+ ownerId: project?.userId || owner?.id || null,
+ },
+ });
+ }, [owner?.id, project?.userId, projectId, title]);
+
const {
isCurrent: isCurrentTrack,
isPlaying: isTrackPlaying,
@@ -150,6 +261,7 @@ const MusicDetails = ({ route }) => {
useEffect(() => {
listenedMsRef.current = 0;
incrementDoneRef.current = false;
+ hasAutoPlayedRef.current = false;
}, [trackId]);
// Start/stop a timer to accumulate listened milliseconds while playing
@@ -282,6 +394,38 @@ const MusicDetails = ({ route }) => {
[trackDescriptor, durationMs, isCurrentTrack, ensureLoaded, seekTrackTo]
);
+ useEffect(() => {
+ if (!autoPlayRequested || hasAutoPlayedRef.current) {
+ return;
+ }
+ if (!trackDescriptor || !songUrl) {
+ return;
+ }
+
+ const run = async () => {
+ try {
+ if (!isCurrentTrack) {
+ await ensureLoaded({ startPositionMs: 0, autoPlay: true });
+ } else if (!isPlaying) {
+ await resumeTrack();
+ }
+ hasAutoPlayedRef.current = true;
+ } catch (e) {
+ console.log("MusicDetails autoplay error", e?.message);
+ }
+ };
+
+ run();
+ }, [
+ autoPlayRequested,
+ ensureLoaded,
+ isCurrentTrack,
+ isPlaying,
+ resumeTrack,
+ songUrl,
+ trackDescriptor,
+ ]);
+
const handleSliderSeekEnd = useCallback(async () => {
const targetMs =
typeof lastSeekTargetMs.current === "number"
@@ -677,6 +821,14 @@ const MusicDetails = ({ route }) => {
backgroundImg={
action === "userProfile" ? background.profileBG : background.libraryBG2
}
+ shareBtn={
+ sharePayload
+ ? {
+ label: "Partager le morceau",
+ onPress: handleShare,
+ }
+ : false
+ }
{...(action === "userProfile" && {
containerStyle: {
backgroundColor: "#0000004D",
@@ -761,6 +913,45 @@ const MusicDetails = ({ route }) => {
resizeMode="contain"
/>
+
+
+
+
+ {isDownloading ? (
+
+ ) : null}
+
+
+
+
SheetManager.show("Playlist", { payload: { projectId } })
@@ -975,3 +1166,38 @@ const styles = StyleSheet.create({
marginBottom: 6,
},
});
+
+const DownloadProgressRing = ({ progress = 0, size = 36, strokeWidth = 3 }) => {
+ const radius = size / 2 - strokeWidth / 2;
+ const circumference = 2 * Math.PI * radius;
+ const clamped = Math.max(0, Math.min(1, progress || 0));
+ const offset = circumference * (1 - clamped);
+
+ return (
+
+
+
+
+ );
+};
diff --git a/src/screens/Playbacks/Playbacks.js b/src/screens/Playbacks/Playbacks.js
index d094f28..0fb01b8 100644
--- a/src/screens/Playbacks/Playbacks.js
+++ b/src/screens/Playbacks/Playbacks.js
@@ -28,6 +28,8 @@ import {
LIKE_TARGET,
toggleProjectLike,
} from "../../utils/likes";
+import { SheetManager } from "react-native-actions-sheet";
+import { Feather } from "@expo/vector-icons";
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
const { currentUID, followUser, unfollowUser } = useUser() || {};
@@ -142,6 +144,18 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
} catch (e) {}
};
}, [videoPlayer]);
+ const handleReport = useCallback(() => {
+ if (!item?.id) return;
+ SheetManager.show("Report", {
+ payload: {
+ targetType: "playback",
+ projectId: item?.id || null,
+ playbackId: item?.id || null,
+ title: item?.title || "",
+ ownerId: item?.userId || null,
+ },
+ });
+ }, [item?.id, item?.title, item?.userId]);
// Build alignedWords from item musicTimestamps
const alignedWords = useMemo(() => {
@@ -365,6 +379,20 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
>
+
+
+
+ Signaler
+
+
{
+ if (!item?.id) return;
+ SheetManager.show("Report", {
+ payload: {
+ targetType: "playback",
+ projectId: item?.id || null,
+ playbackId: item?.id || null,
+ title: item?.title || "",
+ ownerId: item?.userId || null,
+ },
+ });
+ }, [item?.id, item?.title, item?.userId]);
+
const handleLayout = useCallback((event) => {
const { width = 0, height = 0 } = event?.nativeEvent?.layout || {};
setLayoutSize((prev) => ({
@@ -486,6 +501,13 @@ const PlaybackItem = ({
resizeMode="contain"
/>
+
+
+ Signaler
+
diff --git a/src/screens/Studio/GeneratingSong.js b/src/screens/Studio/GeneratingSong.js
index 2a7a844..1ce6154 100644
--- a/src/screens/Studio/GeneratingSong.js
+++ b/src/screens/Studio/GeneratingSong.js
@@ -240,16 +240,7 @@ const GeneratingSong = () => {
if (titleOk && isIdle && !askedRef.current) {
askedRef.current = true;
- AppAlert("Attention", "Une génération va être lancée. Continuer ?", [
- {
- text: "Non",
- style: "cancel",
- onPress: () => {
- askedRef.current = false;
- },
- },
- { text: "Oui", onPress: () => startMusicGenerationOnce() },
- ]);
+ startMusicGenerationOnce();
}
}, [
isFocused,
diff --git a/src/screens/Writing/CreateLyricsWithAi.js b/src/screens/Writing/CreateLyricsWithAi.js
index aa6f20b..c3e0b8b 100644
--- a/src/screens/Writing/CreateLyricsWithAi.js
+++ b/src/screens/Writing/CreateLyricsWithAi.js
@@ -8,8 +8,7 @@ import MusicLandHeader from "../../components/MusicLandHeader";
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
import useLayoutType from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
-import { Routes } from "../../navigation";
-import { goBack, navigate } from "../../navigation/NavigationService";
+import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { sanitizeStructureList } from "../../utils/songStructure";
@@ -23,13 +22,13 @@ import SongStyle from "./SongStyle";
import SongTo from "./SongTo";
import SpecificityContext from "./SpecificityContext";
const { width: windowWidth } = Dimensions.get("window");
+const MAX_STEP_INDEX = 8;
const CreateLyricsWithAi = () => {
const { isWeb } = useLayoutType();
- const { selectedProject, updateProjectData } = useUser();
- const hasLyrics = selectedProject?.hasLyrics === true;
+ const { selectedProject } = useUser();
const scrollRef = useRef(null);
- const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
+ const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
const containerWidth = windowWidth;
@@ -188,13 +187,6 @@ const CreateLyricsWithAi = () => {
}
}, [structure]);
- // Si déjà des paroles, forcer l'accès à partir de l'étape 5 et ignorer 0-4
- React.useEffect(() => {
- if (hasLyrics && selectedIndex < 5) {
- setSelectedIndex(5);
- }
- }, [hasLyrics]);
-
const lyricsConfig = useMemo(() => {
const resolvedObjective = shouldUseOtherObjective
? persistedOtherObjective || undefined
@@ -297,37 +289,7 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = async () => {
- // Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6),
- // sauter Rhymes et CreatingLyrics et aller directement sur Lyrics
- if (hasLyrics && selectedIndex === 6) {
- const chosenStructure = sanitizeStructureList(
- Array.isArray(customStructure) && customStructure.length > 0
- ? customStructure
- : Array.isArray(parsedStructure)
- ? parsedStructure
- : []
- );
- await updateProjectData({
- config: { structure: chosenStructure },
- selections: {
- objective,
- otherObjective: persistedOtherObjective,
- context,
- emotion,
- style,
- otherStyle: persistedOtherStyle,
- audience,
- structure,
- parsedStructure,
- customStructure: sanitizeStructureList(customStructure),
- rhymes,
- },
- hasLyrics: true,
- });
- navigate(Routes.Lyrics);
- return;
- }
- setSelectedIndex((idx) => idx + 1);
+ setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX));
};
const onPressBack = () => {
@@ -344,14 +306,36 @@ const CreateLyricsWithAi = () => {
// Sync scroll position and progress with selectedIndex
React.useEffect(() => {
if (!parentLayout?.height) return;
- try {
- const nextProgress = 16 + selectedIndex * 9;
- setProgress(nextProgress);
- scrollRef.current?.scrollToIndex?.({
- index: selectedIndex,
- animated: true,
- });
- } catch (_) {}
+ const nextProgress = Math.max(0, Math.min(100, 16 + selectedIndex * 9));
+ setProgress(nextProgress);
+
+ const ref = scrollRef.current;
+ if (!ref || typeof ref.scrollToIndex !== "function") {
+ return;
+ }
+
+ let cancelled = false;
+ const attemptScroll = (retries = 0) => {
+ if (cancelled) return;
+ try {
+ ref.scrollToIndex({ index: selectedIndex, animated: true });
+ } catch (error) {
+ if (retries < 3) {
+ setTimeout(() => attemptScroll(retries + 1), 40);
+ } else {
+ console.warn("[CreateLyricsWithAi] scrollToIndex failed", {
+ index: selectedIndex,
+ error,
+ });
+ }
+ }
+ };
+
+ attemptScroll();
+
+ return () => {
+ cancelled = true;
+ };
}, [selectedIndex, parentLayout?.height]);
return (
diff --git a/src/screens/Writing/CreateLyricsWithAi.web.js b/src/screens/Writing/CreateLyricsWithAi.web.js
index 4ec6b3f..325aa8c 100644
--- a/src/screens/Writing/CreateLyricsWithAi.web.js
+++ b/src/screens/Writing/CreateLyricsWithAi.web.js
@@ -6,8 +6,7 @@ import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from "../../data/data";
import Page from "../../layouts/Page";
-import { Routes } from "../../navigation";
-import { goBack, navigate } from "../../navigation/NavigationService";
+import { goBack } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import { sanitizeStructureList } from "../../utils/songStructure";
@@ -21,10 +20,11 @@ import SongStyle from "./SongStyle";
import SongTo from "./SongTo";
import SpecificityContext from "./SpecificityContext";
+const MAX_STEP_INDEX = 8;
+
const CreateLyricsWithAi = () => {
- const { selectedProject, updateProjectData } = useUser();
- const hasLyrics = selectedProject?.hasLyrics === true;
- const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
+ const { selectedProject } = useUser();
+ const [selectedIndex, setSelectedIndex] = useState(0);
// Collected state across steps
const [objective, setObjective] = useState(null); // from Goals list
@@ -187,13 +187,6 @@ const CreateLyricsWithAi = () => {
}
}, [structure]);
- // Si déjà des paroles, forcer l'accès à partir de l'étape 5 et ignorer 0-4
- React.useEffect(() => {
- if (hasLyrics && selectedIndex < 5) {
- setSelectedIndex(5);
- }
- }, [hasLyrics]);
-
const progress = useMemo(
() => 16 + selectedIndex * 9,
[selectedIndex]
@@ -301,37 +294,7 @@ const CreateLyricsWithAi = () => {
]);
const onPressNext = async () => {
- // Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6),
- // sauter Rhymes et CreatingLyrics et aller directement sur Lyrics
- if (hasLyrics && selectedIndex === 6) {
- const chosenStructure = sanitizeStructureList(
- Array.isArray(customStructure) && customStructure.length > 0
- ? customStructure
- : Array.isArray(parsedStructure)
- ? parsedStructure
- : []
- );
- await updateProjectData({
- config: { structure: chosenStructure },
- selections: {
- objective,
- otherObjective: persistedOtherObjective,
- context,
- emotion,
- style,
- otherStyle: persistedOtherStyle,
- audience,
- structure,
- parsedStructure,
- customStructure: sanitizeStructureList(customStructure),
- rhymes,
- },
- hasLyrics: true,
- });
- navigate(Routes.Lyrics);
- return;
- }
- setSelectedIndex((idx) => idx + 1);
+ setSelectedIndex((idx) => Math.min(idx + 1, MAX_STEP_INDEX));
};
const onPressBack = () => {
diff --git a/src/screens/Writing/EmotionConvey.js b/src/screens/Writing/EmotionConvey.js
index 86b2b63..f0bd4c1 100644
--- a/src/screens/Writing/EmotionConvey.js
+++ b/src/screens/Writing/EmotionConvey.js
@@ -3,6 +3,7 @@ import { View } from "react-native";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
import ListSelection from "../../components/ListSelection/ListSelection";
import { EMOTION_CONVEY } from "../../data/data";
+import { strings } from "../../constants/strings";
import CreateLyricsHeader from "./components/CreateLyricsHeader";
const EmotionConvey = ({
@@ -19,8 +20,8 @@ const EmotionConvey = ({
return (
+
+
+ {strings.writing.steps.contextExamples}
+
+
{
}}
>
- Proposition de paroles
-
- Relis attentivement la proposition générée avant de valider.
+
+ {strings.writing.lyrics.title}
-
- Personnalisation :
- {" "}
- modifie le titre et chaque section pour que les paroles te
- ressemblent.
-
-
-
- Nouvelle génération :
- {" "}
- appuie sur "Générer d'autres paroles" si tu souhaites une autre
- suggestion.
+ {strings.writing.lyrics.instructions}
+
+
+ {strings.writing.lyrics.personalizationBanner}
+
+
{
const { createNewProject, selectedProject, updateProjectData } =
useUserData();
const { setIsLoading } = useMinuit();
- async function createAndNavigate({ hasLyrics = false }) {
+ const startWriting = React.useCallback(async () => {
try {
await setIsLoading(true);
if (selectedProject) {
- updateProjectData({ hasLyrics });
+ updateProjectData({ hasLyrics: false });
setTimeout(() => navigate(Routes.CreateLyricsWithAi), 500);
return;
}
- const newProjectId = await createNewProject({ hasLyrics });
+ const newProjectId = await createNewProject({ hasLyrics: false });
if (!newProjectId) {
return;
}
@@ -35,7 +37,7 @@ const WritingLyrics = () => {
} finally {
await setIsLoading(false);
}
- }
+ }, [createNewProject, navigate, selectedProject, setIsLoading, updateProjectData]);
return (
{
progress={9}
logo={icons.musicLandWriting}
/>
-
-
+
+
+
+ {strings.writing.onboarding.heroTitle}
+
+
+ {strings.writing.onboarding.heroSubtitle}
+
+
+
createAndNavigate({ hasLyrics: false })}
+ title={strings.writing.onboarding.primaryCta}
+ onPress={startWriting}
+ textStyle={{ fontFamily: FONT_FAMILY.InterSemiBold }}
/>
createAndNavigate({ hasLyrics: true })}
+ size="small"
+ title={strings.writing.onboarding.secondaryCta}
+ disabled
containerStyle={{ alignSelf: "center", width: "100%" }}
+ maxWidth={360}
/>
+
+ {strings.writing.onboarding.secondaryNote}
+
@@ -85,4 +94,39 @@ const styles = StyleSheet.create({
bottom: -40,
right: -30,
},
+ contentWrapper: {
+ flex: 1,
+ justifyContent: "space-between",
+ paddingTop: gutters * 6,
+ paddingBottom: gutters * 2,
+ paddingHorizontal: gutters,
+ gap: 32,
+ },
+ heroContainer: {
+ gap: 12,
+ maxWidth: 560,
+ },
+ heroTitle: {
+ fontSize: 28,
+ color: Palette.white,
+ fontFamily: FONT_FAMILY.InterSemiBold,
+ },
+ heroSubtitle: {
+ fontSize: 16,
+ lineHeight: 24,
+ color: Palette.white,
+ fontFamily: FONT_FAMILY.InterRegular,
+ opacity: 0.88,
+ },
+ ctaGroup: {
+ gap: 12,
+ },
+ secondaryNote: {
+ textAlign: "center",
+ color: Palette.white,
+ fontFamily: FONT_FAMILY.InterRegular,
+ fontSize: 14,
+ opacity: 0.85,
+ fontStyle: "italic",
+ },
});
diff --git a/src/utils/Sheet.js b/src/utils/Sheet.js
index 4eee952..5da7c38 100644
--- a/src/utils/Sheet.js
+++ b/src/utils/Sheet.js
@@ -7,6 +7,7 @@ import DeleteAudioModal from "../components/modal/DeleteAudioModal";
import PlaylistModal from "../components/modal/PlaylistModal";
import PlaybackPickerModal from "../components/modal/PlaybackPickerModal";
import ShareModal from "../components/modal/ShareModal";
+import ReportModal from "../components/modal/ReportModal";
registerSheet("Delete", DeleteModal);
registerSheet("ProfileSettings", ProfileSettingsModal);
@@ -16,5 +17,6 @@ registerSheet("DeleteAudio", DeleteAudioModal);
registerSheet("Playlist", PlaylistModal);
registerSheet("PlaybackPicker", PlaybackPickerModal);
registerSheet("Share", ShareModal);
+registerSheet("Report", ReportModal);
export {};
diff --git a/src/utils/shareSheet.js b/src/utils/shareSheet.js
index d8ec1b1..84d9b7d 100644
--- a/src/utils/shareSheet.js
+++ b/src/utils/shareSheet.js
@@ -14,6 +14,48 @@ const defaultPayload = {
qrUrl: musiclandShareUrl,
};
+const trimTrailingSlash = (value) =>
+ typeof value === "string" ? value.replace(/\/+$/, "") : "";
+
+const resolveBaseShareUrl = () => {
+ if (typeof window !== "undefined" && window.location?.origin) {
+ const origin = window.location.origin;
+ if (/localhost|127\.0\.0\.1/i.test(origin)) {
+ return origin;
+ }
+ }
+ return musiclandShareUrl;
+};
+
+export const createMusicSharePayload = ({
+ projectId,
+ title,
+ artist,
+} = {}) => {
+ if (!projectId) return null;
+
+ const baseUrl = trimTrailingSlash(resolveBaseShareUrl());
+ const shareUrl = `${baseUrl}/music/${projectId}`;
+
+ let shareMessage = musiclandShareMessage;
+ if (title) {
+ shareMessage = `Découvre "${title}"`;
+ if (artist) {
+ shareMessage += ` de ${artist}`;
+ }
+ shareMessage += " sur MusicLand.";
+ }
+
+ return {
+ heading: "Partager le morceau",
+ shareTitle: title ? `${title} - MusicLand` : "MusicLand",
+ shareMessage,
+ url: shareUrl,
+ qrUrl: shareUrl,
+ linkLabel: shareUrl,
+ };
+};
+
export const openShareSheet = (payload = {}) => {
SheetManager.show("Share", {
payload: {