Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -21,6 +21,7 @@ import { projectsRef, usersRef, videosRef } from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { isWeb } from "../../hooks/useLayoutType.js";
|
||||
import Page from "../../layouts/Page";
|
||||
import LandingPage from "../LandingPage";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation/Routes";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
@@ -54,6 +55,10 @@ const Home = ({ navigation, route }) => {
|
||||
} = useUser();
|
||||
const { setTooltip } = useMinuit();
|
||||
|
||||
if (!currentUID) {
|
||||
return <LandingPage />;
|
||||
}
|
||||
|
||||
const projects = useMemo(
|
||||
() => (Array.isArray(userProjects) ? userProjects : []),
|
||||
[userProjects]
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import useTrackController from "../../hooks/useTrackController";
|
||||
import usePlayer from "../../hooks/usePlayer";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Linking,
|
||||
Pressable,
|
||||
Image as RNImage,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import {
|
||||
@@ -29,21 +33,24 @@ import {
|
||||
usersRef,
|
||||
} from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import usePlayer from "../../hooks/usePlayer";
|
||||
import useTrackController from "../../hooks/useTrackController";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import Style, { gutters, size } from "../../styles/Style";
|
||||
import { getArtistDisplayName } from "../../utils/artistName";
|
||||
import { ensureAuthenticated } from "../../utils/authRedirect";
|
||||
import {
|
||||
createMusicSharePayload,
|
||||
openShareSheet,
|
||||
} from "../../utils/shareSheet";
|
||||
import {
|
||||
formatStructureLabel,
|
||||
getPromptLabelForStructure,
|
||||
getSegmentMeta,
|
||||
normalizeStructureType,
|
||||
} from "../../utils/songStructure";
|
||||
import {
|
||||
createMusicSharePayload,
|
||||
openShareSheet,
|
||||
} from "../../utils/shareSheet";
|
||||
|
||||
// 20 secondes
|
||||
const timeBeforeIncrement = 20000;
|
||||
@@ -413,13 +420,7 @@ const MusicDetails = ({ route }) => {
|
||||
console.log("MusicDetails seekBy error", e?.message);
|
||||
}
|
||||
},
|
||||
[
|
||||
trackDescriptor,
|
||||
positionMs,
|
||||
isCurrentTrack,
|
||||
ensureLoaded,
|
||||
seekTrackBy,
|
||||
]
|
||||
[trackDescriptor, positionMs, isCurrentTrack, ensureLoaded, seekTrackBy]
|
||||
);
|
||||
|
||||
const handleLyricsSeek = useCallback(
|
||||
@@ -495,7 +496,8 @@ const MusicDetails = ({ route }) => {
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim());
|
||||
const stripSectionTag = (txt) => String(txt || "").replace(SECTION_TAG_REGEX, "");
|
||||
const stripSectionTag = (txt) =>
|
||||
String(txt || "").replace(SECTION_TAG_REGEX, "");
|
||||
const parseSectionTag = (txt) => {
|
||||
const match = String(txt || "").match(SECTION_TAG_REGEX);
|
||||
if (!match) return null;
|
||||
@@ -503,7 +505,8 @@ const MusicDetails = ({ route }) => {
|
||||
const lower = label.toLowerCase();
|
||||
let type = "section";
|
||||
if (lower.includes("refrain") || lower.includes("chorus")) type = "refrain";
|
||||
else if (lower.includes("couplet") || lower.includes("verse")) type = "couplet";
|
||||
else if (lower.includes("couplet") || lower.includes("verse"))
|
||||
type = "couplet";
|
||||
else if (
|
||||
lower.includes("pré") ||
|
||||
lower.includes("prechorus") ||
|
||||
@@ -632,7 +635,9 @@ const MusicDetails = ({ route }) => {
|
||||
current = null;
|
||||
return;
|
||||
}
|
||||
const lines = groupAlignedWordsToLines(current.words, { removeTags: true });
|
||||
const lines = groupAlignedWordsToLines(current.words, {
|
||||
removeTags: true,
|
||||
});
|
||||
if (!lines.length) {
|
||||
current = null;
|
||||
return;
|
||||
@@ -785,7 +790,10 @@ const MusicDetails = ({ route }) => {
|
||||
justifyContent: "center",
|
||||
opacity: isCurrentTrack ? 1 : 0.4,
|
||||
}}
|
||||
contentStyle={{ alignItems: "center", justifyContent: "center" }}
|
||||
contentStyle={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Feather
|
||||
name="repeat"
|
||||
@@ -795,7 +803,8 @@ const MusicDetails = ({ route }) => {
|
||||
</PressableScale>
|
||||
<PressableScale
|
||||
onPress={async () => {
|
||||
if (!projectId || !currentUID) return;
|
||||
if (!projectId) return;
|
||||
if (!ensureAuthenticated(currentUID)) return;
|
||||
const next = !fav;
|
||||
setFav(next);
|
||||
try {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
projectsRef,
|
||||
usersRef,
|
||||
} from "../../config/firebase";
|
||||
import { ensureAuthenticated } from "../../utils/authRedirect";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import useTrackController from "../../hooks/useTrackController";
|
||||
import usePlayer from "../../hooks/usePlayer";
|
||||
@@ -1001,7 +1002,8 @@ const MusicDetails = ({ route }) => {
|
||||
</PressableScale>
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
if (!projectId || !currentUID) return;
|
||||
if (!projectId) return;
|
||||
if (!ensureAuthenticated(currentUID)) return;
|
||||
const next = !fav;
|
||||
setFav(next);
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useGlobal } from "reactn";
|
||||
import { icons, img } from "../../../assets";
|
||||
import PressableScale from "../../../components/PressableScale";
|
||||
import { LIKE_TARGET, toggleProjectLike } from "../../../utils/likes";
|
||||
import { ensureAuthenticated } from "../../../utils/authRedirect";
|
||||
import { Palette, Style } from "../../../styles";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { size } from "../../../styles/Style";
|
||||
@@ -55,7 +56,8 @@ const MusicCard = ({
|
||||
}, [JSON.stringify(likedBy), currentUID]);
|
||||
|
||||
const toggleLike = async () => {
|
||||
if (!projectId || !currentUID) return;
|
||||
if (!projectId) return;
|
||||
if (!ensureAuthenticated(currentUID)) return;
|
||||
const next = !selected;
|
||||
setSelected(next);
|
||||
try {
|
||||
|
||||
@@ -8,7 +8,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Image, Platform, Pressable, Share, Text, View } from "react-native";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { icons, img } from "../../assets";
|
||||
@@ -28,6 +28,11 @@ import {
|
||||
LIKE_TARGET,
|
||||
toggleProjectLike,
|
||||
} from "../../utils/likes";
|
||||
import { ensureAuthenticated } from "../../utils/authRedirect";
|
||||
import {
|
||||
createPlaybackSharePayload,
|
||||
openShareSheet,
|
||||
} from "../../utils/shareSheet";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
|
||||
@@ -109,7 +114,13 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
|
||||
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
|
||||
|
||||
const creatorName = item?.userName;
|
||||
const creatorName = useMemo(() => {
|
||||
if (owner?.displayName) return owner.displayName;
|
||||
if (owner?.artistName) return owner.artistName;
|
||||
if (owner?.userName) return owner.userName;
|
||||
if (typeof item?.userName === "string") return item.userName;
|
||||
return "";
|
||||
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
|
||||
|
||||
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
||||
p.loop = false;
|
||||
@@ -182,7 +193,23 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
return () => global.clearInterval(id);
|
||||
}, [isActive, videoPlayer]);
|
||||
|
||||
// partage: géré via l'API native Share directement dans l'UI
|
||||
const sharePayload = useMemo(() => {
|
||||
if (!item?.id) return null;
|
||||
return createPlaybackSharePayload({
|
||||
projectId: item.id,
|
||||
title: projectTitle || undefined,
|
||||
artist: creatorName || undefined,
|
||||
playbackUrl: videoUrl || undefined,
|
||||
});
|
||||
}, [creatorName, item?.id, projectTitle, videoUrl]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
if (sharePayload) {
|
||||
openShareSheet(sharePayload);
|
||||
}
|
||||
}, [sharePayload]);
|
||||
|
||||
// partage: via feuille unifiée (prend en charge QR)
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -234,11 +261,20 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
imageProps={{ priority: "high" }}
|
||||
/>
|
||||
</Pressable>
|
||||
{owner?.id && currentUID && owner.id !== currentUID && (
|
||||
{owner?.id && owner.id !== currentUID && (
|
||||
<Pressable
|
||||
disabled={isFollowActionPending}
|
||||
onPress={async () => {
|
||||
if (isFollowActionPending) return;
|
||||
if (
|
||||
!ensureAuthenticated(currentUID, {
|
||||
onIntercept: () => {
|
||||
setIsFollowActionPending(false);
|
||||
},
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFollowActionPending(true);
|
||||
@@ -301,7 +337,10 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (!currentUID || !item?.id) return;
|
||||
if (!item?.id) return;
|
||||
if (!ensureAuthenticated(currentUID)) {
|
||||
return;
|
||||
}
|
||||
const nextLiked = !isLiked;
|
||||
setIsLiked(nextLiked);
|
||||
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
||||
@@ -358,25 +397,7 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
const url = item?.playbackUrl || "";
|
||||
const title = item?.title || "Partager";
|
||||
const base = item?.title
|
||||
? `Découvre « ${item.title} » sur MusicLand`
|
||||
: `Découvre ce playback sur MusicLand`;
|
||||
const message = url ? `${base}\n${url}` : base;
|
||||
|
||||
await Share.share(
|
||||
Platform.select({
|
||||
ios: url ? { url, message, title } : { message, title },
|
||||
default: { message, title },
|
||||
})
|
||||
);
|
||||
} catch (e) {}
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={handleShare}>
|
||||
<Image source={icons.share} style={size({ size: 26 })} />
|
||||
</Pressable>
|
||||
<Pressable onPress={handleReport} style={{ alignItems: "center" }}>
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Palette } from "../../../styles";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { size } from "../../../styles/Style";
|
||||
import { getArtistDisplayName } from "../../../utils/artistName";
|
||||
import { ensureAuthenticated } from "../../../utils/authRedirect";
|
||||
|
||||
moment.locale("fr");
|
||||
|
||||
@@ -58,6 +59,10 @@ const CommentsPanel = ({
|
||||
);
|
||||
const [text, setText] = useState("");
|
||||
const scrollRef = useRef(null);
|
||||
const requireAuth = useCallback(
|
||||
() => ensureAuthenticated(currentUID),
|
||||
[currentUID]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setText("");
|
||||
@@ -111,7 +116,8 @@ const CommentsPanel = ({
|
||||
|
||||
const onSend = useCallback(async () => {
|
||||
const value = (text || "").trim();
|
||||
if (!value || !projectId || !currentUID) return;
|
||||
if (!value || !projectId) return;
|
||||
if (!requireAuth()) return;
|
||||
try {
|
||||
setText("");
|
||||
const docRef = await projectsRef
|
||||
@@ -153,6 +159,7 @@ const CommentsPanel = ({
|
||||
currentUserData,
|
||||
onCommentAdded,
|
||||
projectId,
|
||||
requireAuth,
|
||||
setComments,
|
||||
text,
|
||||
]);
|
||||
@@ -226,23 +233,42 @@ const CommentsPanel = ({
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
onChangeText={(value) => {
|
||||
if (!requireAuth()) {
|
||||
return;
|
||||
}
|
||||
setText(value);
|
||||
}}
|
||||
placeholder={
|
||||
canComment
|
||||
? "Commente"
|
||||
: "Connecte-toi pour laisser un commentaire"
|
||||
}
|
||||
placeholderTextColor="#FFFFFF90"
|
||||
editable={canComment}
|
||||
editable
|
||||
onFocus={() => {
|
||||
if (!requireAuth()) {
|
||||
try {
|
||||
inputRef.current?.blur?.();
|
||||
} catch (_e) {}
|
||||
}
|
||||
}}
|
||||
onPressIn={() => {
|
||||
if (!requireAuth()) {
|
||||
try {
|
||||
inputRef.current?.blur?.();
|
||||
} catch (_e) {}
|
||||
}
|
||||
}}
|
||||
style={[styles.commentInput, !canComment && { opacity: 0.6 }]}
|
||||
/>
|
||||
<Pressable
|
||||
onPress={onSend}
|
||||
disabled={!canComment || !text.trim()}
|
||||
disabled={!text.trim()}
|
||||
style={({ pressed }) => [
|
||||
styles.sendButton,
|
||||
(!canComment || !text.trim()) && styles.sendButtonDisabled,
|
||||
pressed && canComment && styles.sendButtonPressed,
|
||||
!text.trim() && styles.sendButtonDisabled,
|
||||
pressed && text.trim() && styles.sendButtonPressed,
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
|
||||
@@ -9,7 +9,6 @@ import React, {
|
||||
import {
|
||||
Image,
|
||||
Pressable,
|
||||
Share,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
@@ -30,6 +29,11 @@ import {
|
||||
LIKE_TARGET,
|
||||
toggleProjectLike,
|
||||
} from "../../../utils/likes";
|
||||
import { ensureAuthenticated } from "../../../utils/authRedirect";
|
||||
import {
|
||||
createPlaybackSharePayload,
|
||||
openShareSheet,
|
||||
} from "../../../utils/shareSheet";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
|
||||
@@ -254,6 +258,30 @@ const PlaybackItem = ({
|
||||
const descriptionText =
|
||||
item?.description || item?.title || "Description chanson";
|
||||
|
||||
const creatorName = useMemo(() => {
|
||||
if (owner?.displayName) return owner.displayName;
|
||||
if (owner?.artistName) return owner.artistName;
|
||||
if (owner?.userName) return owner.userName;
|
||||
if (typeof item?.userName === "string") return item.userName;
|
||||
return "";
|
||||
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
|
||||
|
||||
const sharePayload = useMemo(() => {
|
||||
if (!item?.id) return null;
|
||||
return createPlaybackSharePayload({
|
||||
projectId: item.id,
|
||||
title: typeof item?.title === "string" ? item.title.trim() : undefined,
|
||||
artist: creatorName || undefined,
|
||||
playbackUrl: videoUrl || undefined,
|
||||
});
|
||||
}, [creatorName, item?.id, item?.title, videoUrl]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
if (sharePayload) {
|
||||
openShareSheet(sharePayload);
|
||||
}
|
||||
}, [sharePayload]);
|
||||
|
||||
const handleReport = useCallback(() => {
|
||||
if (!item?.id) return;
|
||||
SheetManager.show("Report", {
|
||||
@@ -400,10 +428,19 @@ const PlaybackItem = ({
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
{owner?.id && currentUID && owner.id !== currentUID && (
|
||||
{owner?.id && owner.id !== currentUID && (
|
||||
<Pressable
|
||||
style={styles.followPressable}
|
||||
onPress={async () => {
|
||||
if (
|
||||
!ensureAuthenticated(currentUID, {
|
||||
onIntercept: () => {
|
||||
setIsFollowing(false);
|
||||
},
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const next = !isFollowing;
|
||||
setIsFollowing(next);
|
||||
@@ -438,7 +475,10 @@ const PlaybackItem = ({
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (!currentUID || !item?.id) return;
|
||||
if (!item?.id) return;
|
||||
if (!ensureAuthenticated(currentUID)) {
|
||||
return;
|
||||
}
|
||||
const nextLiked = !isLiked;
|
||||
setIsLiked(nextLiked);
|
||||
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
||||
@@ -481,18 +521,7 @@ const PlaybackItem = ({
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
const url = item?.playbackUrl || "";
|
||||
const title = item?.title || "Partager";
|
||||
const base = item?.title
|
||||
? `Découvre « ${item.title} » sur MusicLand`
|
||||
: `Découvre ce playback sur MusicLand`;
|
||||
const message = url ? `${base}\n${url}` : base;
|
||||
|
||||
await Share.share({ message, title, url });
|
||||
} catch (_e) {}
|
||||
}}
|
||||
onPress={handleShare}
|
||||
style={[styles.actionButton, styles.actionSpacing]}
|
||||
>
|
||||
<Image
|
||||
|
||||
@@ -33,12 +33,12 @@ export default ({ navigation }) => {
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
// Pas d'utilisateur connecté => vers Login
|
||||
// Pas d'utilisateur connecté => accueil avec tab bar
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: Routes.LandingPage,
|
||||
name: Routes.BottomTab,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user