diff --git a/src/components/FeatureCarousel/FeatureCarousel.js b/src/components/FeatureCarousel/FeatureCarousel.js index fc4c4ea..ae0861f 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.js +++ b/src/components/FeatureCarousel/FeatureCarousel.js @@ -40,6 +40,12 @@ const STAGE_CARD_CONTENT = [ description: "Come back when your audio is ready!", image: ai.rightIcon, }, + { + key: "publisher", + title: "Bena", + description: "Ta vidéo est prête ? Direction YouTube !", + image: ai.bena, + }, ]; const WEB_SCROLL_INACTIVE_DELTA = 0.05; diff --git a/src/components/FeatureCarousel/FeatureCarousel.web.js b/src/components/FeatureCarousel/FeatureCarousel.web.js index 361a176..15337b1 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.web.js +++ b/src/components/FeatureCarousel/FeatureCarousel.web.js @@ -39,6 +39,12 @@ const STAGE_CARD_CONTENT = [ description: "Come back when your audio is ready!", image: ai.rightIcon, }, + { + key: "publisher", + title: "Bena", + description: "Ta vidéo est prête ? Direction YouTube !", + image: ai.bena, + }, ]; const WEB_SCROLL_INACTIVE_DELTA = 0.05; diff --git a/src/navigation/MainStack.js b/src/navigation/MainStack.js index ee12762..d0d47b9 100644 --- a/src/navigation/MainStack.js +++ b/src/navigation/MainStack.js @@ -20,6 +20,7 @@ import PlaybackOnboarding from "../screens/Playback/PlaybackOnboarding"; import RecordPlayback from "../screens/Playback/RecordPlayback"; import RecordedPlayback from "../screens/Playback/RecordedPlayback"; import VideoFinalize from "../screens/Playback/VideoFinalize"; +import PublishYoutube from "../screens/Publishing/PublishYoutube"; import PrivacyPolicy from "../screens/PrivacyPolicy"; import DownloadPrices from "../screens/Production/DownloadPrices"; import DownloadSongs from "../screens/Production/DownloadSongs"; @@ -228,6 +229,10 @@ const baseScreens = [ name: Routes.VideoFinalize, component: VideoFinalize, }, + { + name: Routes.PublishYoutube, + component: PublishYoutube, + }, { name: Routes.AllMyPlaylist, component: AllMyPlaylist, diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js index a476493..cf2f147 100644 --- a/src/navigation/Routes.js +++ b/src/navigation/Routes.js @@ -58,6 +58,7 @@ export const Routes = { ChooseDecor: "ChooseDecor", CreatingDecor: "CreatingDecor", VideoFinalize: "VideoFinalize", + PublishYoutube: "PublishYoutube", Create: "Create", HitParade: "HitParade", diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js index 660f092..e7f6c52 100644 --- a/src/screens/NewMusicOptions.js +++ b/src/screens/NewMusicOptions.js @@ -36,6 +36,14 @@ const CREATE_DATA = [ desc: "Come back when your\naudio is ready!", type: "Director", }, + { + stageKey: "publisher", + img: ai.bena, + bg: background.productionBG2, + label: "Bena", + desc: "Publions ta vidéo sur YouTube !", + type: "Producteur", + }, ]; const NewMusicOptions = () => { diff --git a/src/screens/Publishing/PublishYoutube.js b/src/screens/Publishing/PublishYoutube.js new file mode 100644 index 0000000..7b84cfc --- /dev/null +++ b/src/screens/Publishing/PublishYoutube.js @@ -0,0 +1,261 @@ +import { useRoute } from "@react-navigation/native"; +import { Image } from "expo-image"; +import React, { useCallback, useMemo, useState } from "react"; +import { Linking, Text, View } from "react-native"; +import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; +import { ai, background } from "../../assets"; +import BorderGradientButton from "../../components/BorderGradientButton"; +import GradientButton from "../../components/GradientButton"; +import MusicLandHeader from "../../components/MusicLandHeader"; +import firebase, { projectsRef, serverTimestamp } from "../../config/firebase"; +import Page from "../../layouts/Page"; +import { goBack } from "../../navigation/NavigationService"; +import { useUser } from "../../providers/UserDataProvider"; +import { Palette, gutters } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; + +const PublishYoutube = () => { + const route = useRoute(); + const routeProjectId = route?.params?.projectId ?? null; + const { userProjects = [], selectedProject } = useUser(); + const { setTooltip } = useMinuit(); + + const project = useMemo(() => { + if (routeProjectId) { + return ( + userProjects.find((item) => item?.id === routeProjectId) || + selectedProject || + null + ); + } + if (selectedProject?.id) { + return selectedProject; + } + return userProjects.length ? userProjects[0] : null; + }, [routeProjectId, selectedProject, userProjects]); + + const projectId = project?.id || routeProjectId || null; + + const [saving, setSaving] = useState(false); + const youtubeUrl = project?.youtubeUrl ?? null; + const youtubeStatus = project?.youtubeStatus ?? null; + const youtubeError = project?.youtubeError ?? null; + const playbackReady = !!project?.playbackUrl; + const hasYoutubePublication = !!youtubeUrl; + const isPublishing = useMemo( + () => + ["PUBLISHING", "UPLOADING", "PROCESSING", "QUEUED"].includes( + youtubeStatus + ), + [youtubeStatus] + ); + const publishDisabled = + saving || isPublishing || !playbackReady || !projectId; + const publishButtonLabel = isPublishing + ? "Publication en cours..." + : hasYoutubePublication + ? "Mettre à jour sur YouTube" + : "Publier sur YouTube"; + const statusMessage = useMemo(() => { + if (!playbackReady) { + return "Termine la création de ton playback avec John pour débloquer la publication YouTube."; + } + if (isPublishing) { + return "Bena s'occupe de mettre ta vidéo en ligne... Patiente un instant."; + } + if (youtubeError || youtubeStatus === "FAILED") { + return "La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous."; + } + if (hasYoutubePublication) { + return "Ta vidéo est publiée sur la chaîne YouTube MusicLand. Tu peux la partager dès maintenant."; + } + return "Bena t’accompagne pour partager ton playback sur la chaîne YouTube MusicLand."; + }, [ + hasYoutubePublication, + isPublishing, + playbackReady, + youtubeError, + youtubeStatus, + ]); + + const handleOpenYoutube = useCallback(async () => { + if (!youtubeUrl) { + return; + } + try { + await Linking.openURL(youtubeUrl); + } catch (_error) { + setTooltip?.({ + type: "error", + text: "Impossible d'ouvrir le lien YouTube", + }); + } + }, [setTooltip, youtubeUrl]); + + const handleMarkPublished = useCallback(async () => { + if (!projectId) { + setTooltip?.({ + type: "error", + text: "Sélectionnez un projet avant de publier", + }); + return; + } + if (!playbackReady) { + setTooltip?.({ + type: "error", + text: "Aucun playback n'est disponible pour la publication", + }); + return; + } + setSaving(true); + try { + const publishCallable = firebase + .functions() + .httpsCallable("publishPlaybackToYoutube"); + await publishCallable({ projectId }); + setTooltip?.({ + type: "success", + text: "Publication lancée sur YouTube", + }); + } catch (error) { + setTooltip?.({ + type: "error", + text: error?.message || "Impossible de lancer la publication", + }); + } finally { + setSaving(false); + } + }, [playbackReady, projectId, setTooltip]); + + const handleResetPublication = useCallback(async () => { + if (!projectId) { + setTooltip?.({ + type: "error", + text: "Sélectionnez un projet avant de réinitialiser", + }); + return; + } + setSaving(true); + try { + await projectsRef.doc(projectId).set( + { + youtubePublished: false, + youtubeUrl: null, + youtubeVideoId: null, + youtubePublishedAt: null, + youtubeStatus: "IDLE", + youtubeError: null, + updatedAt: serverTimestamp(), + }, + { merge: true } + ); + setTooltip?.({ + type: "success", + text: "Publication YouTube réinitialisée", + }); + } catch (error) { + setTooltip?.({ + type: "error", + text: error?.message || "Réinitialisation impossible", + }); + } finally { + setSaving(false); + } + }, [projectId, setTooltip]); + + return ( + + + + + + + {hasYoutubePublication + ? "Ta vidéo est déjà sur YouTube" + : "Publier sur YouTube"} + + + {statusMessage} + + {youtubeError && ( + + {youtubeError} + + )} + {hasYoutubePublication && youtubeUrl && ( + + {youtubeUrl} + + )} + + + + + {hasYoutubePublication && youtubeUrl && ( + + )} + {hasYoutubePublication && ( + + )} + + + + ); +}; + +export default PublishYoutube; diff --git a/src/utils/projectStages.js b/src/utils/projectStages.js index f14b478..1abf34e 100644 --- a/src/utils/projectStages.js +++ b/src/utils/projectStages.js @@ -1,6 +1,11 @@ import { Routes } from "../navigation/Routes"; -export const CREATION_STAGE_KEYS = ["songwriter", "beatmaker", "director"]; +export const CREATION_STAGE_KEYS = [ + "songwriter", + "beatmaker", + "director", + "publisher", +]; const getLyricsCount = (project) => { if (!project) return 0; @@ -16,6 +21,16 @@ const getStageMetadata = (project) => { const hasCoverUrl = !!project?.coverUrl; const hasPlaybackAsset = !!project?.playbackUrl; const hasPlayback = hasPlaybackAsset || hasSongUrl; + const youtubeUrl = project?.youtubeUrl ?? null; + const youtubeStatus = project?.youtubeStatus ?? null; + const youtubeError = project?.youtubeError ?? null; + const hasYoutubePublication = !!youtubeUrl; + const isYoutubePublishing = [ + "PUBLISHING", + "UPLOADING", + "PROCESSING", + "QUEUED", + ].includes(youtubeStatus); const musicUrls = Array.isArray(project?.musicUrls) ? project.musicUrls.filter(Boolean) : []; @@ -38,9 +53,13 @@ const getStageMetadata = (project) => { musicStatus, coverStatus, playbackStatus, + youtubeStatus, + youtubeError, isMusicGenerating: musicStatus === "GENERATING", isCoverGenerating: coverStatus === "GENERATING", isPlaybackGenerating, + hasYoutubePublication, + isYoutubePublishing, }; }; @@ -55,6 +74,8 @@ const getStageLockState = (key, metadata) => { return metadata.lyricsCount <= 0; case "director": return !metadata.hasCover; + case "publisher": + return !metadata.hasPlaybackAsset; default: return true; } @@ -111,6 +132,20 @@ const getStageDescription = (key, metadata) => { return "Modifier le playback généré"; } return "Commencer la création de votre playback"; + case "publisher": + if (!hasPlaybackAsset) { + return "Créez un playback pour débloquer la publication"; + } + if (metadata.isYoutubePublishing) { + return "Publication de votre vidéo en cours"; + } + if (metadata.youtubeError) { + return "La publication a échoué, réessayez."; + } + if (metadata.hasYoutubePublication) { + return "Votre vidéo est en ligne et prête à être partagée"; + } + return "Publier votre vidéo sur la chaîne YouTube MusicLand"; default: return ""; } @@ -133,6 +168,13 @@ const getStageLockedDescription = (key, metadata) => { return metadata.hasPlaybackAsset ? "Impossible de modifier le playback" : undefined; + case "publisher": + if (!metadata.hasPlaybackAsset) { + return "Générez un playback pour débloquer la publication"; + } + return metadata.hasYoutubePublication + ? "La vidéo est déjà publiée" + : undefined; default: return undefined; } @@ -146,6 +188,8 @@ const getStageCompletionState = (key, metadata) => { return metadata.hasSongUrl || metadata.musicStatus === "GENERATED"; case "director": return metadata.hasPlaybackAsset; + case "publisher": + return metadata.hasYoutubePublication; default: return false; } @@ -224,6 +268,15 @@ export const getStageAction = (key, project) => { route: Routes.Playback, params: project ? { project } : undefined, }; + case "publisher": { + if (!project?.id) { + return { route: Routes.PublishYoutube }; + } + return { + route: Routes.PublishYoutube, + params: { projectId: project.id }, + }; + } default: return {}; } diff --git a/youtube.md b/youtube.md new file mode 100644 index 0000000..f2aea31 --- /dev/null +++ b/youtube.md @@ -0,0 +1,24 @@ +https://accounts.google.com/o/oauth2/v2/auth +?client_id=305598753437-21ivo6din58aud9dph9bcob06fb52r5n.apps.googleusercontent.com +&redirect_uri=http://localhost:8081 +&response_type=code +&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.upload +&access_type=offline +&prompt=consent + +http://localhost:8081/?code=4/0Ab32j90LMDeJDShWSk1OkXwgbjrHS-qQpyYTlXhH9q3nlV0EJa6cTL-7nI9yOKHoz6rgRA&scope=https://www.googleapis.com/auth/youtube.upload + +curl -X POST https://oauth2.googleapis.com/token \ + -d client_id=305598753437-21ivo6din58aud9dph9bcob06fb52r5n.apps.googleusercontent.com \ + -d client_secret=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 \ + -d code=4/0Ab32j90LMDeJDShWSk1OkXwgbjrHS-qQpyYTlXhH9q3nlV0EJa6cTL-7nI9yOKHoz6rgRA \ + -d grant_type=authorization_code \ + -d redirect_uri=http://localhost:8081 + +{ +"access_token": "ya29.a0ATi6K2uR4DhnmTDl0EN2tvDogwwEwl8SDJLpUzvnZ8LGgOYjgmhqg7MoMM0_iTJR_NBEDPe2LlJDqCFFnIi9s3qRv_zfw_NeTnN-LwUmAyVJkdqSP6GvzQCuZkE6OBuTvaU5b2obk5rHvDOkWXxAb7xjbVTzIBIg8O5VVhJ-VB8VEbTVsSq9bc0ljkbcyBEzm4JdQRwaCgYKAcgSAQ8SFQHGX2MiR1Owf2aiiEBUsU-LDUDgWg0206", +"expires_in": 3599, +"refresh_token": "1//03sy-LDVmCMcFCgYIARAAGAMSNwF-L9IrHwwdBBZvg3Hwun9hxHUx_AsJaa1qeozmtvGtdOXKIRIyKjTp7S_b0igvzM_1A5bLH34", +"scope": "https://www.googleapis.com/auth/youtube.upload", +"token_type": "Bearer" +}