diff --git a/src/components/RotationBorder/RotationBorder.js b/src/components/RotationBorder/RotationBorder.js
new file mode 100644
index 0000000..57eb88e
--- /dev/null
+++ b/src/components/RotationBorder/RotationBorder.js
@@ -0,0 +1,20 @@
+import React from "react";
+
+import "./rotationTestStyle.css";
+
+// Web-only rotating gradient border container
+// Props:
+// - active: show rotating gradient when true (default)
+// - style, className: optional DOM styles/classes
+const RotationBorder = ({ children, style, className, active = true }) => {
+ const classes = ["box", active ? "a" : null, className]
+ .filter(Boolean)
+ .join(" ");
+ return (
+
+ {children}
+
+ );
+};
+
+export default RotationBorder;
diff --git a/src/components/RotationBorder/rotationTestStyle.css b/src/components/RotationBorder/rotationTestStyle.css
new file mode 100644
index 0000000..18c6634
--- /dev/null
+++ b/src/components/RotationBorder/rotationTestStyle.css
@@ -0,0 +1,42 @@
+.box {
+ --border-angle: 0deg;
+ border-radius: 12px;
+ width: 100%;
+ height: 260px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-shadow: 0px 2px 4px hsl(0 0% 0% / 25%);
+ animation: border-angle-rotate 5s infinite linear;
+ border: 2px solid transparent;
+ position: relative;
+
+ &.a {
+ background: linear-gradient(#11012f, #11012f) padding-box,
+ conic-gradient(
+ from var(--border-angle),
+ oklch(0.6659 0.2211 304.21),
+ oklch(0.6659 0.2211 304.21 / 0%),
+ oklch(0.6659 0.2211 304.21),
+ oklch(0.6659 0.2211 304.21 / 0%),
+ oklch(0.6659 0.2211 304.21)
+
+ )
+ border-box;
+ }
+}
+
+@keyframes border-angle-rotate {
+ from {
+ --border-angle: 0deg;
+ }
+ to {
+ --border-angle: 360deg;
+ }
+}
+
+@property --border-angle {
+ syntax: "";
+ initial-value: 0deg;
+ inherits: false;
+}
diff --git a/src/config/initialGlobalState.js b/src/config/initialGlobalState.js
index 1376e26..68b9b25 100644
--- a/src/config/initialGlobalState.js
+++ b/src/config/initialGlobalState.js
@@ -14,6 +14,9 @@ export default {
title: "",
},
+ _isLoading: false,
+ _loadingMessage: null,
+
_config: {
colors: {
primary: Palette.primary,
diff --git a/src/config/loaderMessages.js b/src/config/loaderMessages.js
new file mode 100644
index 0000000..2ff900b
--- /dev/null
+++ b/src/config/loaderMessages.js
@@ -0,0 +1,8 @@
+const loaderMessages = {
+ globalDefault: "",
+ profilePhotoUploadWeb: "",
+ photoCoverGenerationWeb: "",
+ pouchReadyGenerationWeb: "",
+};
+
+export default loaderMessages;
diff --git a/src/hooks/useGlobalLoading.js b/src/hooks/useGlobalLoading.js
new file mode 100644
index 0000000..ac0963f
--- /dev/null
+++ b/src/hooks/useGlobalLoading.js
@@ -0,0 +1,27 @@
+import { useCallback } from "react";
+import { useGlobal } from "reactn";
+
+const useGlobalLoading = () => {
+ const [, setIsLoading] = useGlobal("_isLoading");
+ const [, setLoadingMessage] = useGlobal("_loadingMessage");
+
+ const setLoading = useCallback(
+ async (loading, { message } = {}) => {
+ if (loading) {
+ await setLoadingMessage(message ?? null);
+ await setIsLoading(true);
+ } else {
+ await setIsLoading(false);
+ await setLoadingMessage(null);
+ }
+ },
+ [setIsLoading, setLoadingMessage]
+ );
+
+ return {
+ setLoading,
+ setLoadingMessage,
+ };
+};
+
+export default useGlobalLoading;
diff --git a/src/providers/LoadingProvider.js b/src/providers/LoadingProvider.js
index 84734bb..de98474 100644
--- a/src/providers/LoadingProvider.js
+++ b/src/providers/LoadingProvider.js
@@ -1,13 +1,15 @@
+import { ActivityIndicator, Text, View } from "react-native";
import React, { useGlobal } from "reactn";
-import { ActivityIndicator, View } from "react-native";
-import { Palette } from "../styles";
import useLayoutType from "../hooks/useLayoutType";
-
+import { Palette } from "../styles";
+import { FONT_FAMILY } from "../styles/Fonts";
export default ({ children }) => {
const [isGlobalLoading] = useGlobal("_isLoading");
+ const [loadingMessage] = useGlobal("_loadingMessage");
const { isDesktopWeb = false, isMobileWeb = false } = useLayoutType();
+ const isWeb = isDesktopWeb || isMobileWeb;
return (
<>
@@ -30,17 +32,35 @@ export default ({ children }) => {
isDesktopWeb
? { position: "fixed" }
: isMobileWeb
- ? { position: "fixed" }
- : { position: "absolute" },
+ ? { position: "fixed" }
+ : { position: "absolute" },
]}
>
-
+
)}
>
);
};
-export const LoaderIndicator = () => {
- return ;
+export const LoaderIndicator = ({ isWeb = false, message = null }) => {
+ return (
+
+
+ {isWeb && message ? (
+
+ {message}
+
+ ) : null}
+
+ );
};
diff --git a/src/screens/Profile/Profile.js b/src/screens/Profile/Profile.js
index 701c782..ade1b3c 100644
--- a/src/screens/Profile/Profile.js
+++ b/src/screens/Profile/Profile.js
@@ -1,8 +1,10 @@
import { useRoute } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import { Image as ExpoImage } from "expo-image";
+import * as ImagePicker from "expo-image-picker";
import React, { useEffect, useMemo, useState } from "react";
import {
+ ActivityIndicator,
FlatList,
Image,
Pressable,
@@ -18,13 +20,19 @@ import { background, icons, img } from "../../assets";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import MoreMenu from "../../components/MoreMenu";
import PressableScale from "../../components/PressableScale";
-import { projectsRef, usersRef } from "../../config/firebase";
+import firebase, {
+ projectsRef,
+ serverTimestamp,
+ usersRef,
+} from "../../config/firebase";
+import loaderMessages from "../../config/loaderMessages";
+import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import useLayoutType from "../../hooks/useLayoutType";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
-import { navigate, push } from "../../navigation/NavigationService";
+import { push } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
@@ -54,6 +62,8 @@ const Profile = () => {
const [menuPosition, setMenuPosition] = useState(null);
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
+ const [updatingPhoto, setUpdatingPhoto] = useState(false);
+ const [webUploadMessage, setWebUploadMessage] = useState("");
const navigateToMusicDetails = useNavigateToMusicDetails();
const onPressMenu = (item) => {
setSelected(item);
@@ -114,6 +124,59 @@ const Profile = () => {
}
};
+ const onChangeProfilePicture = async (message = "") => {
+ if (!isSelf || updatingPhoto) return;
+
+ try {
+ setUpdatingPhoto(true);
+ setWebUploadMessage(isWeb && message ? message : "");
+ const uid = currentUID;
+ if (!uid) return;
+
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: true,
+ aspect: [4, 4],
+ quality: 1,
+ });
+
+ const pickedUri = result?.assets?.[0]?.uri || null;
+ if (!pickedUri) return;
+
+ const { resultURI = null } = await uploadFileToFirebase({
+ uri: pickedUri,
+ path: `users/${uid}/profilePicture.png`,
+ });
+
+ if (!resultURI) {
+ throw new Error("Téléversement de l'image impossible");
+ }
+
+ await usersRef.doc(uid).set(
+ {
+ profilePictureURL: resultURI,
+ updatedAt: serverTimestamp(),
+ },
+ { merge: true }
+ );
+
+ const authUser = firebase.auth().currentUser;
+ if (authUser) {
+ await authUser.updateProfile({ photoURL: resultURI });
+ }
+
+ setTooltip({ text: "Photo de profil mise à jour", type: "success" });
+ } catch (e) {
+ setTooltip({
+ text: e?.message || "Erreur changement photo de profil",
+ type: "error",
+ });
+ } finally {
+ setUpdatingPhoto(false);
+ setWebUploadMessage("");
+ }
+ };
+
// Composant factorisé pour afficher la liste de musiques
const MusicListSection = ({ data, emptyText }) => (
@@ -286,30 +349,59 @@ const Profile = () => {
)}
-
-
+
+
+
+ {isSelf && (
+
+ onChangeProfilePicture(
+ loaderMessages.profilePhotoUploadWeb
+ )
+ }
+ disabled={updatingPhoto}
+ hitSlop={10}
+ >
+
+ {updatingPhoto ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ {isWeb && updatingPhoto && webUploadMessage ? (
+ {webUploadMessage}
+ ) : null}
{
useUserData();
const { setIsLoading } = useMinuit();
const isGenerating = selectedProject?.coverStatus === "GENERATING";
+ const coverGenerationMessage = isWeb
+ ? loaderMessages.photoCoverGenerationWeb
+ : "";
const onChooseForegroundImage = async () => {
try {
@@ -102,9 +106,31 @@ const PhotoCover = () => {
{isGenerating && (
-
- Génération en cours.
-
+ {isWeb ? (
+ coverGenerationMessage ? (
+
+ {coverGenerationMessage}
+
+ ) : null
+ ) : (
+
+ Génération en cours.
+
+ )}
)}
diff --git a/src/screens/cover/PouchReady.js b/src/screens/cover/PouchReady.js
index 531effd..ca73e53 100644
--- a/src/screens/cover/PouchReady.js
+++ b/src/screens/cover/PouchReady.js
@@ -8,6 +8,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import firebase, { tasksRef } from "../../config/firebase";
+import loaderMessages from "../../config/loaderMessages";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
@@ -21,6 +22,9 @@ const PouchReady = () => {
const { setIsLoading } = useMinuit();
const isGenerating = selectedProject?.coverStatus === "GENERATING";
const isFocused = useIsFocused();
+ const coverBackgroundMessage = isWeb
+ ? loaderMessages.pouchReadyGenerationWeb
+ : "";
const generateCover = useCallback(async () => {
if (!selectedProjectId) return;
@@ -126,9 +130,31 @@ const PouchReady = () => {
{isGenerating && (
-
- Génération en cours.
-
+ {isWeb ? (
+ coverBackgroundMessage ? (
+
+ {coverBackgroundMessage}
+
+ ) : null
+ ) : (
+
+ Génération en cours.
+
+ )}
)}