fix tickets add reort mail and design fixes
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
</Pressable>
|
||||
<PressableScale onPress={handleReport}>
|
||||
<Feather
|
||||
name="flag"
|
||||
size={22}
|
||||
color={Palette.white}
|
||||
style={{ marginHorizontal: 2 }}
|
||||
/>
|
||||
</PressableScale>
|
||||
<View
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<DownloadProgressRing
|
||||
progress={downloadProgress}
|
||||
size={36}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
) : null}
|
||||
<Pressable
|
||||
onPress={handleDownload}
|
||||
disabled={isDownloading || !songUrl}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
opacity: isDownloading || !songUrl ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<Feather name="download" size={18} color={Palette.white} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
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 (
|
||||
<Svg
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ position: "absolute", top: 0, left: 0 }}
|
||||
>
|
||||
<Circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="rgba(255, 255, 255, 0.15)"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="transparent"
|
||||
/>
|
||||
<Circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke={Palette.primary}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
fill="transparent"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user