share modal

This commit is contained in:
2025-10-03 13:36:18 +02:00
parent 8ea7f89c7b
commit dcc50506de
13 changed files with 377 additions and 54 deletions
+2
View File
@@ -37,6 +37,7 @@ import Providers from "./src/providers";
import { LogBox } from "react-native"; import { LogBox } from "react-native";
import CommentsBottomSheet from "./src/components/bottomsheets/CommentsBottomSheet"; import CommentsBottomSheet from "./src/components/bottomsheets/CommentsBottomSheet";
import "./src/utils/Sheet"; import "./src/utils/Sheet";
import ShareQrModalContainer from "./src/components/modal/ShareQrModalContainer";
console.disableYellowBox = true; console.disableYellowBox = true;
console.reportErrorsAsExceptions = false; console.reportErrorsAsExceptions = false;
@@ -173,6 +174,7 @@ const App = () => {
<MainStack /> <MainStack />
</NavigationContainer> </NavigationContainer>
<CommentsBottomSheet /> <CommentsBottomSheet />
<ShareQrModalContainer />
{/* </AppLayout> */} {/* </AppLayout> */}
</MenuProvider> </MenuProvider>
</Providers> </Providers>
+2 -1
View File
@@ -3,6 +3,7 @@ import React from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import ActionSheet from "react-native-actions-sheet"; import ActionSheet from "react-native-actions-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { isWeb } from "../hooks/useLayoutType";
import { gutters, Palette } from "../styles"; import { gutters, Palette } from "../styles";
const AppActionSheet = ({ children, props }) => { const AppActionSheet = ({ children, props }) => {
@@ -19,7 +20,7 @@ const AppActionSheet = ({ children, props }) => {
{...props} {...props}
> >
<BlurView <BlurView
intensity={Platform.OS === "ios" ? 20 : 10} intensity={Platform.OS === "ios" || isWeb ? 20 : 10}
style={{ style={{
paddingTop: 36, paddingTop: 36,
paddingHorizontal: 14, paddingHorizontal: 14,
+5 -5
View File
@@ -1,10 +1,10 @@
import { View, Text, Pressable, Image, Platform } from "react-native";
import React from "react";
import BorderGradient from "./BorderGradient/BorderGradient";
import { Palette, Style } from "../styles";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import React from "react";
import { Image, Pressable, Text, View } from "react-native";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts"; import { FONT_FAMILY } from "../styles/Fonts";
import { size } from "../styles/Style"; import { size } from "../styles/Style";
import BorderGradient from "./BorderGradient/BorderGradient";
const BorderGradientButton = ({ const BorderGradientButton = ({
title = "Jai déjà mes paroles", title = "Jai déjà mes paroles",
@@ -45,7 +45,7 @@ const BorderGradientButton = ({
}} }}
> >
<BlurView <BlurView
intensity={Platform.OS === "ios" ? 40 : 10} intensity={40}
tint={tint} tint={tint}
style={{ style={{
width: "100%", width: "100%",
+61
View File
@@ -0,0 +1,61 @@
import React, { useMemo } from "react";
import { View } from "react-native";
import Svg, { Rect } from "react-native-svg";
import { generateQrMatrix } from "../utils/generateQrMatrix";
const DEFAULT_SIZE = 220;
const QrCode = ({
value,
size = DEFAULT_SIZE,
color = "#FFFFFF",
backgroundColor = "transparent",
errorCorrectionLevel = "M",
}) => {
const { matrix } = useMemo(() => {
return generateQrMatrix({ value, errorCorrectionLevel });
}, [value, errorCorrectionLevel]);
const cellSize = useMemo(() => {
if (!matrix || !matrix.length) {
return 0;
}
return size / matrix.length;
}, [matrix, size]);
return (
<View
style={{
width: size,
height: size,
backgroundColor,
}}
>
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
{matrix.map((row, rowIndex) => {
return row.map((isDark, colIndex) => {
if (!isDark) {
return null;
}
const key = `${rowIndex}-${colIndex}`;
return (
<Rect
key={key}
x={colIndex * cellSize}
y={rowIndex * cellSize}
width={cellSize}
height={cellSize}
fill={color}
/>
);
});
})}
</Svg>
</View>
);
};
export default QrCode;
+75 -31
View File
@@ -1,3 +1,8 @@
import {
Feather,
FontAwesome,
MaterialCommunityIcons,
} from "@expo/vector-icons";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@@ -15,21 +20,18 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import {
Feather,
FontAwesome,
MaterialCommunityIcons,
} from "@expo/vector-icons";
import AppActionSheet from "../AppActionSheet"; import { BlurView } from "expo-blur";
import { Palette } from "../../styles"; import { setGlobal } from "reactn";
import { FONT_FAMILY } from "../../styles/Fonts";
import { isWeb } from "../../hooks/useLayoutType";
import { import {
musiclandShareHeading, musiclandShareHeading,
musiclandShareMessage, musiclandShareMessage,
musiclandShareUrl, musiclandShareUrl,
} from "../../data"; } from "../../data";
import { isWeb } from "../../hooks/useLayoutType";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import AppActionSheet from "../AppActionSheet";
const ShareModal = ({ payload }) => { const ShareModal = ({ payload }) => {
const heading = payload?.heading ?? musiclandShareHeading; const heading = payload?.heading ?? musiclandShareHeading;
@@ -38,9 +40,12 @@ const ShareModal = ({ payload }) => {
const shareMessage = payload?.shareMessage ?? musiclandShareMessage; const shareMessage = payload?.shareMessage ?? musiclandShareMessage;
const linkLabel = payload?.linkLabel ?? url; const linkLabel = payload?.linkLabel ?? url;
const sectionTitle = payload?.sectionTitle ?? "Partager sur..."; const sectionTitle = payload?.sectionTitle ?? "Partager sur...";
const qrTitle = payload?.qrTitle ?? "Scannez le QR-code";
const qrUrl = payload?.qrUrl ?? musiclandShareUrl;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const copyTimeout = useRef(null); const copyTimeout = useRef(null);
const qrOpenTimeout = useRef(null);
const closeSheet = useCallback(() => { const closeSheet = useCallback(() => {
SheetManager.hide("Share"); SheetManager.hide("Share");
@@ -61,6 +66,9 @@ const ShareModal = ({ payload }) => {
if (copyTimeout.current) { if (copyTimeout.current) {
clearTimeout(copyTimeout.current); clearTimeout(copyTimeout.current);
} }
if (qrOpenTimeout.current) {
clearTimeout(qrOpenTimeout.current);
}
}; };
}, []); }, []);
@@ -123,6 +131,22 @@ const ShareModal = ({ payload }) => {
const handleShareOption = useCallback( const handleShareOption = useCallback(
async (option) => { async (option) => {
if (option?.type === "qr") {
const qrTitleValue = qrTitle;
closeSheet();
qrOpenTimeout.current = setTimeout(() => {
setGlobal({
shareQrModal: {
visible: true,
url: qrUrl,
title: qrTitleValue,
},
});
}, 200);
return;
}
const target = option?.target; const target = option?.target;
if (!target) { if (!target) {
@@ -151,7 +175,7 @@ const ShareModal = ({ payload }) => {
await handleSystemShare(); await handleSystemShare();
closeSheet(); closeSheet();
}, },
[closeSheet, handleSystemShare] [closeSheet, handleSystemShare, qrTitle, qrUrl]
); );
const onSystemShare = useCallback(async () => { const onSystemShare = useCallback(async () => {
@@ -193,11 +217,11 @@ const ShareModal = ({ payload }) => {
}, },
{ {
key: "qr", key: "qr",
type: "qr",
label: "Partager par QR-code", label: "Partager par QR-code",
Icon: (props) => ( Icon: (props) => (
<MaterialCommunityIcons name="qrcode-scan" {...props} /> <MaterialCommunityIcons name="qrcode-scan" {...props} />
), ),
target: `https://api.qrserver.com/v1/create-qr-code/?size=512x512&data=${encodedUrl}`,
}, },
]; ];
}, [encodedShareText, encodedUrl, payload?.options, shareTitle]); }, [encodedShareText, encodedUrl, payload?.options, shareTitle]);
@@ -205,7 +229,12 @@ const ShareModal = ({ payload }) => {
return ( return (
<AppActionSheet id="Share"> <AppActionSheet id="Share">
<View style={styles.container}> <View style={styles.container}>
<View style={styles.card}> <BlurView
style={[styles.card, styles.blurCard]}
tint="dark"
intensity={40}
experimentalBlurMethod="dimezisBlurView"
>
<Text style={styles.heading}>{heading}</Text> <Text style={styles.heading}>{heading}</Text>
<View style={styles.linkRow}> <View style={styles.linkRow}>
<Text numberOfLines={1} style={styles.linkText}> <Text numberOfLines={1} style={styles.linkText}>
@@ -232,31 +261,39 @@ const ShareModal = ({ payload }) => {
</Pressable> </Pressable>
</View> </View>
</View> </View>
{copied ? ( {copied ? <Text style={styles.copiedText}>Lien copié</Text> : null}
<Text style={styles.copiedText}>Lien copie</Text> </BlurView>
) : null}
</View>
<View style={styles.card}> <BlurView
intensity={70}
experimentalBlurMethod="dimezisBlurView"
style={styles.card}
>
<Text style={styles.sectionHeading}>{sectionTitle}</Text> <Text style={styles.sectionHeading}>{sectionTitle}</Text>
<View style={styles.optionsList}> <View style={styles.optionsList}>
{shareOptions.map((option) => { {shareOptions.map((option, index) => {
const Icon = option?.Icon ?? Feather; const Icon = option?.Icon ?? Feather;
return ( return (
<Pressable <BlurView
key={option?.key} key={option?.key || index}
onPress={() => handleShareOption(option)} style={styles.optionBlur}
style={styles.optionRow} intensity={40}
experimentalBlurMethod="dimezisBlurView"
> >
<View style={styles.optionIconWrapper}> <Pressable
<Icon size={18} color={Palette.white} /> onPress={() => handleShareOption(option)}
</View> style={styles.optionRow}
<Text style={styles.optionLabel}>{option?.label}</Text> >
</Pressable> <View style={styles.optionIconWrapper}>
<Icon size={18} color={Palette.white} />
</View>
<Text style={styles.optionLabel}>{option?.label}</Text>
</Pressable>
</BlurView>
); );
})} })}
</View> </View>
</View> </BlurView>
</View> </View>
</AppActionSheet> </AppActionSheet>
); );
@@ -267,7 +304,8 @@ const styles = StyleSheet.create({
gap: 24, gap: 24,
}, },
card: { card: {
backgroundColor: Palette.glass, // backgroundColor: Palette.glass,
overflow: "hidden",
borderRadius: 24, borderRadius: 24,
borderWidth: 1, borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.08)", borderColor: "rgba(255, 255, 255, 0.08)",
@@ -275,6 +313,9 @@ const styles = StyleSheet.create({
paddingVertical: 18, paddingVertical: 18,
gap: 16, gap: 16,
}, },
blurCard: {
overflow: "hidden",
},
heading: { heading: {
fontSize: 20, fontSize: 20,
color: Palette.white, color: Palette.white,
@@ -325,11 +366,15 @@ const styles = StyleSheet.create({
optionsList: { optionsList: {
gap: 12, gap: 12,
}, },
optionBlur: {
borderRadius: 18,
overflow: "hidden",
},
optionRow: { optionRow: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.04)", backgroundColor: "rgba(255, 255, 255, 0.04)",
borderRadius: 18, // borderRadius: 18,
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 14, paddingVertical: 14,
gap: 14, gap: 14,
@@ -351,4 +396,3 @@ const styles = StyleSheet.create({
}); });
export default ShareModal; export default ShareModal;
+116
View File
@@ -0,0 +1,116 @@
import React, { useMemo } from "react";
import { BlurView } from "expo-blur";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { Feather } from "@expo/vector-icons";
import { Portal } from "@gorhom/portal";
import QrCode from "../QrCode";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
const ShareQrModal = ({ visible, onClose, url, title = "Scannez le QR-code" }) => {
const shareUrl = useMemo(() => url, [url]);
if (!visible) {
return null;
}
const blurIntensity = Platform.select({ ios: 40, default: 20 });
return (
<Portal>
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
<Pressable
onPress={onClose}
style={[StyleSheet.absoluteFill, styles.backdrop]}
/>
<View style={styles.centerWrapper} pointerEvents="box-none">
<BlurView intensity={blurIntensity} tint="dark" style={styles.modalCard}>
<Pressable onPress={onClose} style={styles.closeButton} hitSlop={10}>
<Feather name="x" size={20} color={Palette.white} />
</Pressable>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>
Partage ce code pour guider tes contacts directement vers MusicLand.
</Text>
<View style={styles.qrWrapper}>
<QrCode value={shareUrl} size={220} backgroundColor="transparent" />
</View>
<Text numberOfLines={1} ellipsizeMode="middle" style={styles.linkText}>
{shareUrl}
</Text>
</View>
</BlurView>
</View>
</View>
</Portal>
);
};
const styles = StyleSheet.create({
backdrop: {
backgroundColor: "rgba(0, 0, 0, 0.65)",
},
centerWrapper: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 24,
},
modalCard: {
width: 360,
maxWidth: "90%",
borderRadius: 28,
overflow: "hidden",
paddingHorizontal: 24,
paddingVertical: 32,
backgroundColor: Palette.glass,
},
closeButton: {
position: "absolute",
top: 18,
right: 18,
width: 36,
height: 36,
borderRadius: 18,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(255, 255, 255, 0.12)",
},
content: {
alignItems: "center",
gap: 18,
},
title: {
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
},
subtitle: {
fontSize: 14,
color: Palette.white,
opacity: 0.8,
textAlign: "center",
fontFamily: FONT_FAMILY.InterRegular,
paddingHorizontal: 8,
},
qrWrapper: {
padding: 16,
borderRadius: 24,
backgroundColor: "rgba(255, 255, 255, 0.08)",
},
linkText: {
fontSize: 13,
color: Palette.white,
opacity: 0.9,
textAlign: "center",
fontFamily: FONT_FAMILY.InterMedium,
},
});
export default ShareQrModal;
@@ -0,0 +1,30 @@
import React from "react";
import { useGlobal } from "reactn";
import { musiclandShareUrl } from "../../data";
import ShareQrModal from "./ShareQrModal";
const ShareQrModalContainer = () => {
const [shareQrModal, setShareQrModal] = useGlobal("shareQrModal");
const { visible, url, title } = shareQrModal || {};
const handleClose = () => {
setShareQrModal({
visible: false,
url: null,
title: "",
});
};
return (
<ShareQrModal
visible={!!visible}
url={url || musiclandShareUrl}
title={title || "Scannez le QR-code"}
onClose={handleClose}
/>
);
};
export default ShareQrModalContainer;
+6
View File
@@ -8,6 +8,12 @@ export default {
webBackgroundImg: null, webBackgroundImg: null,
webLayoutMode: "default", webLayoutMode: "default",
shareQrModal: {
visible: false,
url: null,
title: "",
},
_config: { _config: {
colors: { colors: {
primary: Palette.primary, primary: Palette.primary,
+1 -1
View File
@@ -187,7 +187,7 @@ const HitParade = () => {
backgroundImg={isWeb ? background.libraryBgWeb : background.hitParadeBG} backgroundImg={isWeb ? background.libraryBgWeb : background.hitParadeBG}
headerType="NONE" headerType="NONE"
width="100%" width="100%"
maxWidth={null} maxWidth={800}
blurIntensity={isWeb ? 2 : 0} blurIntensity={isWeb ? 2 : 0}
> >
<View style={{ gap: 12, flex: 1 }}> <View style={{ gap: 12, flex: 1 }}>
+36 -4
View File
@@ -1,18 +1,23 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useGlobal } from "reactn";
import MoreMenu from "../../../components/MoreMenu";
import { projectsRef } from "../../../config/firebase";
import { Routes } from "../../../navigation"; import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService"; import { navigate } from "../../../navigation/NavigationService";
import { useUser } from "../../../providers/UserDataProvider"; import { useUser } from "../../../providers/UserDataProvider";
import CardContainer from "./CardContainer"; import CardContainer from "./CardContainer";
import MusicCard from "./MusicCard"; import MusicCard from "./MusicCard";
import MoreMenu from "../../../components/MoreMenu";
const MyMusic = () => { const MyMusic = () => {
const { userProjects = [] } = useUser(); const { userProjects = [] } = useUser();
const [menuPosition, setMenuPosition] = useState(null); const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null); const [selectedProjectId, setSelectedProjectId] = useState(null);
const projects = Array.isArray(userProjects) ? userProjects.slice(0, 3) : []; const projects = Array.isArray(userProjects) ? userProjects.slice(0, 3) : [];
const [, setTooltip] = useGlobal("_tooltip");
return ( return (
<CardContainer <CardContainer
label="Mes musiques" label="Mes musiques"
@@ -39,8 +44,7 @@ const MyMusic = () => {
setSelectedProjectId(p.id); setSelectedProjectId(p.id);
setMenuPosition(posTop); setMenuPosition(posTop);
setShowMenu( setShowMenu(
(prev) => (prev) => !prev || posTop?.top !== (menuPosition?.top ?? null)
!prev || posTop?.top !== (menuPosition?.top ?? null),
); );
}} }}
/> />
@@ -52,6 +56,34 @@ const MyMusic = () => {
onClose={() => setShowMenu(false)} onClose={() => setShowMenu(false)}
inPlaylist={false} inPlaylist={false}
projectId={selectedProjectId} projectId={selectedProjectId}
extraItems={[
{
label: "Supprimer",
onPress: () =>
SheetManager.show("Delete", {
payload: {
title: "Supprimer le projet",
message:
"Cette action supprimera définitivement ce projet.",
onConfirm: 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",
});
}
},
},
}),
},
]}
/> />
</View> </View>
</CardContainer> </CardContainer>
+5 -11
View File
@@ -1,16 +1,10 @@
import { import { BlurView } from "expo-blur";
View,
Text,
TextInput,
Image,
Pressable,
Platform,
} from "react-native";
import React, { useState } from "react"; import React, { useState } from "react";
import { Platform, Pressable, Text, TextInput, View } from "react-native";
import EyeSlashSVG from "../../../assets/UI/EyeSlashSVG";
import EyeSVG from "../../../assets/UI/EyeSVG";
import { Palette, Style } from "../../../styles"; import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts"; import { FONT_FAMILY } from "../../../styles/Fonts";
import { BlurView } from "expo-blur";
import { icons } from "../../../assets";
const EditInput = ({ const EditInput = ({
label = "", label = "",
@@ -79,7 +73,7 @@ const EditInput = ({
/> />
{type === "password" && ( {type === "password" && (
<Pressable onPress={() => setShowPassword(!showPassword)}> <Pressable onPress={() => setShowPassword(!showPassword)}>
<Image source={icons.eye} /> {showPassword ? <EyeSVG /> : <EyeSlashSVG />}
</Pressable> </Pressable>
)} )}
</BlurView> </BlurView>
+37
View File
@@ -0,0 +1,37 @@
import QRCode from "qrcode-terminal/vendor/QRCode";
import QRErrorCorrectLevel from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel";
const getErrorLevel = (level) => {
if (typeof level === "number") {
return level;
}
const normalized = String(level || "M").toUpperCase();
return QRErrorCorrectLevel?.[normalized] ?? QRErrorCorrectLevel.M;
};
export const generateQrMatrix = ({
value,
errorCorrectionLevel = "M",
}) => {
if (!value) {
throw new Error("generateQrMatrix requires a value");
}
const qr = new QRCode(-1, getErrorLevel(errorCorrectionLevel));
qr.addData(value);
qr.make();
const size = qr.getModuleCount();
const matrix = new Array(size);
for (let row = 0; row < size; row += 1) {
matrix[row] = new Array(size);
for (let col = 0; col < size; col += 1) {
matrix[row][col] = qr.isDark(row, col);
}
}
return { matrix, size };
};
+1 -1
View File
@@ -11,6 +11,7 @@ const defaultPayload = {
url: musiclandShareUrl, url: musiclandShareUrl,
shareTitle: "MusicLand", shareTitle: "MusicLand",
shareMessage: musiclandShareMessage, shareMessage: musiclandShareMessage,
qrUrl: musiclandShareUrl,
}; };
export const openShareSheet = (payload = {}) => { export const openShareSheet = (payload = {}) => {
@@ -21,4 +22,3 @@ export const openShareSheet = (payload = {}) => {
}, },
}); });
}; };