724 lines
23 KiB
JavaScript
724 lines
23 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { Image as ExpoImage } from 'expo-image'
|
|
import {
|
|
BackHandler,
|
|
Linking,
|
|
Platform,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
Alert,
|
|
} from 'react-native'
|
|
import { useFocusEffect } from '@react-navigation/native'
|
|
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
|
import * as FileSystem from 'expo-file-system'
|
|
import * as Sharing from 'expo-sharing'
|
|
import GradientButton from '../../components/GradientButton'
|
|
import MusicLandHeader from '../../components/MusicLandHeader'
|
|
import Page from '../../layouts/Page'
|
|
import { Routes } from '../../navigation/Routes'
|
|
import { goBack, navigate, push } from '../../navigation/NavigationService'
|
|
import { useUser } from '../../providers/UserDataProvider'
|
|
import { useStripe } from '../../providers/StripeProvider'
|
|
import { gutters, Palette, Style } from '../../styles'
|
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
|
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
|
import { background } from '../../assets'
|
|
import { getStageAction } from '../../utils/projectStages'
|
|
import ClubAdvantagesCard from '../Profile/components/ClubAdvantagesCard'
|
|
import useGlobalLoading from '../../hooks/useGlobalLoading'
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons'
|
|
import { isWeb } from '../../hooks/useLayoutType'
|
|
import { getArtistDisplayName } from '../../utils/artistName'
|
|
import { toDate } from '../../utils/dateFormatting'
|
|
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
|
import AdventureChoiceModal from './components/AdventureChoiceModal'
|
|
import ShareAndCreditsModal from './components/ShareAndCreditsModal'
|
|
import { musiclandShareHeading } from '../../data'
|
|
|
|
const SongDownload = ({ route }) => {
|
|
const {
|
|
project: routeProject,
|
|
selectedOption: routeSelectedOption,
|
|
coverOptions: routeCoverOptions,
|
|
skipAdventureGate = false,
|
|
promptPurchaseConfirm = false,
|
|
backRoute = null,
|
|
} = route?.params || {}
|
|
const { selectedProject, updateProjectData, hasActiveSubscription, hasPurchased } = useUser()
|
|
const { createSongDownloadCheckout } = useStripe()
|
|
const { setTooltip } = useMinuit()
|
|
const { setLoading } = useGlobalLoading()
|
|
const [isDownloading, setIsDownloading] = useState(false)
|
|
const [isCheckoutLaunching, setIsCheckoutLaunching] = useState(false)
|
|
const [downloadPaymentPending, setDownloadPaymentPending] = useState(false)
|
|
const { videos } = useUser()
|
|
const part1Url = isWeb ? videos?.benaiPart1 : videos?.benaiPart1
|
|
const [showIntro, setShowIntro] = useState(null)
|
|
const [showAdventureModal, setShowAdventureModal] = useState(false)
|
|
const [showShareModal, setShowShareModal] = useState(false)
|
|
const [adventureChoice, setAdventureChoice] = useState(() =>
|
|
skipAdventureGate ? 'stop' : 'pending'
|
|
)
|
|
const adventureGateTriggeredRef = useRef(false)
|
|
const reopenAdventureModalOnFocusRef = useRef(false)
|
|
|
|
const projectForStage = useMemo(() => {
|
|
if (routeProject?.id && selectedProject?.id && routeProject.id === selectedProject.id) {
|
|
return selectedProject
|
|
}
|
|
return routeProject || selectedProject || null
|
|
}, [routeProject, selectedProject])
|
|
|
|
const coverOptions = useMemo(() => {
|
|
if (Array.isArray(routeCoverOptions) && routeCoverOptions.length) {
|
|
return routeCoverOptions.filter(Boolean)
|
|
}
|
|
if (Array.isArray(projectForStage?.cover?.options)) {
|
|
return projectForStage.cover.options.filter(Boolean)
|
|
}
|
|
return []
|
|
}, [projectForStage?.cover?.options, routeCoverOptions])
|
|
|
|
const selectedOption = useMemo(() => {
|
|
if (routeSelectedOption) {
|
|
return routeSelectedOption
|
|
}
|
|
const selectedId =
|
|
projectForStage?.cover?.selectedOptionId || projectForStage?.cover?.selectedOption?.id || null
|
|
if (selectedId && coverOptions.length) {
|
|
const match = coverOptions.find((option) => option?.id === selectedId)
|
|
if (match) return match
|
|
}
|
|
return coverOptions[0] || null
|
|
}, [coverOptions, projectForStage?.cover, routeSelectedOption])
|
|
|
|
const coverUrl =
|
|
selectedOption?.finalUrl ||
|
|
selectedOption?.generatedUrl ||
|
|
projectForStage?.coverUrl ||
|
|
projectForStage?.cover?.result ||
|
|
projectForStage?.cover?.generatedBackground ||
|
|
null
|
|
const trackTitle =
|
|
typeof projectForStage?.title === 'string' && projectForStage.title.trim()
|
|
? projectForStage.title.trim()
|
|
: 'Musicland Track'
|
|
const projectId = projectForStage?.id || null
|
|
const downloadPurchaseStatus = projectForStage?.downloadPurchase?.status || null
|
|
const hasPaidDownload = downloadPurchaseStatus === 'paid'
|
|
const canDownloadDirectly = hasActiveSubscription || hasPaidDownload || hasPurchased
|
|
const downloadLabel = canDownloadDirectly
|
|
? 'Télécharger mon morceau'
|
|
: 'Télécharger ce morceau pour 1,99€'
|
|
const shouldShowDownloadPage = adventureChoice === 'stop'
|
|
|
|
const reopenAdventureChoice = useCallback(() => {
|
|
reopenAdventureModalOnFocusRef.current = false
|
|
setShowIntro(null)
|
|
setAdventureChoice('pending')
|
|
setShowAdventureModal(true)
|
|
}, [])
|
|
|
|
const handleBackPress = useCallback(() => {
|
|
if (shouldShowDownloadPage) {
|
|
reopenAdventureChoice()
|
|
return true
|
|
}
|
|
if (backRoute) {
|
|
navigate(backRoute)
|
|
return true
|
|
}
|
|
goBack()
|
|
return true
|
|
}, [backRoute, reopenAdventureChoice, shouldShowDownloadPage])
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
if (Platform.OS !== 'android' || (!backRoute && !shouldShowDownloadPage)) {
|
|
return undefined
|
|
}
|
|
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress)
|
|
return () => subscription.remove()
|
|
}, [backRoute, handleBackPress, shouldShowDownloadPage])
|
|
)
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
if (!reopenAdventureModalOnFocusRef.current) {
|
|
return undefined
|
|
}
|
|
setShowIntro(null)
|
|
setShowAdventureModal(true)
|
|
return undefined
|
|
}, [])
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (adventureGateTriggeredRef.current) {
|
|
return
|
|
}
|
|
if (adventureChoice !== 'pending') {
|
|
return
|
|
}
|
|
if (typeof videos === 'undefined') {
|
|
return
|
|
}
|
|
adventureGateTriggeredRef.current = true
|
|
if (part1Url) {
|
|
setShowIntro(part1Url)
|
|
} else {
|
|
setShowAdventureModal(true)
|
|
}
|
|
}, [adventureChoice, part1Url, videos])
|
|
|
|
useEffect(() => {
|
|
if (adventureChoice !== 'pending') {
|
|
return
|
|
}
|
|
if (!adventureGateTriggeredRef.current) {
|
|
return
|
|
}
|
|
if (showIntro) {
|
|
return
|
|
}
|
|
if (!showAdventureModal) {
|
|
setShowAdventureModal(true)
|
|
}
|
|
}, [adventureChoice, showAdventureModal, showIntro])
|
|
|
|
const continueFlow = useCallback(async () => {
|
|
if (!selectedOption) {
|
|
return
|
|
}
|
|
try {
|
|
await setLoading(true, { message: 'Sauvegarde de ta pochette...' })
|
|
const existingCover = projectForStage?.cover || {}
|
|
const finalUrl = selectedOption.finalUrl || selectedOption.generatedUrl || coverUrl
|
|
const nextCoverData = {
|
|
...existingCover,
|
|
options: coverOptions,
|
|
selectedOptionId: selectedOption.id,
|
|
result: finalUrl,
|
|
generatedBackground: selectedOption.generatedUrl || selectedOption.finalUrl || null,
|
|
}
|
|
await updateProjectData({
|
|
cover: nextCoverData,
|
|
coverUrl: finalUrl,
|
|
})
|
|
const nextProject = {
|
|
...projectForStage,
|
|
cover: nextCoverData,
|
|
coverUrl: finalUrl,
|
|
}
|
|
const nextPlaybackStage = getStageAction('director', nextProject)
|
|
const targetRoute = nextPlaybackStage?.route || Routes.Playback
|
|
const params = nextPlaybackStage?.params || { project: nextProject }
|
|
push(targetRoute, {
|
|
...params,
|
|
returnToAdventureModal: true,
|
|
})
|
|
} catch (error) {
|
|
console.log('[SongDownload] continue error', error?.message)
|
|
} finally {
|
|
await setLoading(false)
|
|
}
|
|
}, [
|
|
coverOptions,
|
|
coverUrl,
|
|
projectForStage,
|
|
push,
|
|
selectedOption,
|
|
setLoading,
|
|
updateProjectData,
|
|
])
|
|
|
|
const handleCloseIntro = useCallback(() => {
|
|
setShowIntro(null)
|
|
setShowAdventureModal(true)
|
|
}, [])
|
|
|
|
const handleContinueAdventure = useCallback(() => {
|
|
reopenAdventureModalOnFocusRef.current = true
|
|
setShowAdventureModal(false)
|
|
setAdventureChoice('continue')
|
|
continueFlow()
|
|
}, [continueFlow])
|
|
|
|
const handleDownload = useCallback(async () => {
|
|
const downloadUrl =
|
|
projectForStage?.songUrl ||
|
|
projectForStage?.playbackUrl ||
|
|
selectedOption?.finalUrl ||
|
|
selectedOption?.generatedUrl ||
|
|
null
|
|
if (!downloadUrl || isDownloading) {
|
|
return
|
|
}
|
|
|
|
await setLoading(true, { message: 'Préparation du téléchargement...' })
|
|
const artist = getArtistDisplayName(projectForStage, 'MusicLand')
|
|
const createdDate = toDate(projectForStage?.createdAt) || new Date()
|
|
const createdLabel = createdDate ? createdDate.toISOString().split('T')[0] : ''
|
|
|
|
const triggerWebDownload = async (url, title) => {
|
|
setIsDownloading(true)
|
|
await setLoading(true, { message: 'Préparation du téléchargement...' })
|
|
try {
|
|
const response = await fetch(url)
|
|
if (!response.ok) {
|
|
throw new Error(`download_failed_${response.status}`)
|
|
}
|
|
const contentType = response.headers.get('content-type') || 'audio/mpeg'
|
|
const baseName =
|
|
String(trackTitle || 'musicland-track')
|
|
.replace(/[\\/:*?"<>|]+/g, '-')
|
|
.trim() || 'musicland-track'
|
|
const filename = `${baseName}.mp3`
|
|
const buffer = await response.arrayBuffer()
|
|
|
|
if (contentType.includes('audio')) {
|
|
const toSynchSafe = (size) => {
|
|
const out = new Uint8Array(4)
|
|
out[0] = (size >> 21) & 0x7f
|
|
out[1] = (size >> 14) & 0x7f
|
|
out[2] = (size >> 7) & 0x7f
|
|
out[3] = size & 0x7f
|
|
return out
|
|
}
|
|
|
|
const concatBytes = (...arrays) => {
|
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
|
|
const result = new Uint8Array(totalLength)
|
|
let offset = 0
|
|
arrays.forEach((arr) => {
|
|
result.set(arr, offset)
|
|
offset += arr.length
|
|
})
|
|
return result
|
|
}
|
|
|
|
const buildTextFrame = (id, value) => {
|
|
const encoder = new TextEncoder()
|
|
const textBytes = encoder.encode(value || '')
|
|
const data = concatBytes(new Uint8Array([0x03]), textBytes)
|
|
const header = concatBytes(
|
|
new TextEncoder().encode(id),
|
|
toSynchSafe(data.length),
|
|
new Uint8Array([0x00, 0x00])
|
|
)
|
|
return concatBytes(header, data)
|
|
}
|
|
|
|
const buildApicFrame = (imageBytes, mime) => {
|
|
if (!imageBytes) return null
|
|
const encoder = new TextEncoder()
|
|
const mimeBytes = encoder.encode(mime || 'image/jpeg')
|
|
const data = concatBytes(
|
|
new Uint8Array([0x03]),
|
|
mimeBytes,
|
|
new Uint8Array([0x00]), // mime terminator
|
|
new Uint8Array([0x03]), // front cover
|
|
new Uint8Array([0x00]), // empty description
|
|
new Uint8Array(imageBytes)
|
|
)
|
|
const header = concatBytes(
|
|
new TextEncoder().encode('APIC'),
|
|
toSynchSafe(data.length),
|
|
new Uint8Array([0x00, 0x00])
|
|
)
|
|
return concatBytes(header, data)
|
|
}
|
|
|
|
const buildId3Tag = (audioBytes, coverBytes, coverMime) => {
|
|
const frames = []
|
|
frames.push(buildTextFrame('TIT2', trackTitle))
|
|
frames.push(buildTextFrame('TPE1', artist))
|
|
frames.push(buildTextFrame('TDRC', createdLabel))
|
|
const apic = buildApicFrame(coverBytes, coverMime)
|
|
if (apic) frames.push(apic)
|
|
|
|
const framesData = concatBytes(...frames)
|
|
const header = concatBytes(
|
|
new TextEncoder().encode('ID3'),
|
|
new Uint8Array([0x04, 0x00]), // version 2.4.0
|
|
new Uint8Array([0x00]), // flags
|
|
toSynchSafe(framesData.length)
|
|
)
|
|
return concatBytes(header, framesData, audioBytes)
|
|
}
|
|
|
|
const fetchCoverBytes = async () => {
|
|
if (!coverUrl) return { bytes: null, mime: null }
|
|
try {
|
|
const res = await fetch(coverUrl)
|
|
const mime = res.headers?.get('content-type') || 'image/jpeg'
|
|
const bufferImage = await res.arrayBuffer()
|
|
return { bytes: new Uint8Array(bufferImage), mime }
|
|
} catch {
|
|
return { bytes: null, mime: null }
|
|
}
|
|
}
|
|
|
|
const { bytes: coverBytes, mime: coverMime } = await fetchCoverBytes()
|
|
const merged = buildId3Tag(new Uint8Array(buffer), coverBytes, coverMime)
|
|
const blob = new Blob([merged], { type: contentType })
|
|
const blobUrl = URL.createObjectURL(blob)
|
|
const downloadLink = document.createElement('a')
|
|
downloadLink.href = blobUrl
|
|
downloadLink.download = filename
|
|
document.body.appendChild(downloadLink)
|
|
downloadLink.click()
|
|
document.body.removeChild(downloadLink)
|
|
URL.revokeObjectURL(blobUrl)
|
|
} else {
|
|
const blob = await response.blob()
|
|
const blobUrl = URL.createObjectURL(blob)
|
|
const downloadLink = document.createElement('a')
|
|
downloadLink.href = blobUrl
|
|
downloadLink.download = filename
|
|
document.body.appendChild(downloadLink)
|
|
downloadLink.click()
|
|
document.body.removeChild(downloadLink)
|
|
URL.revokeObjectURL(blobUrl)
|
|
}
|
|
} finally {
|
|
setIsDownloading(false)
|
|
}
|
|
}
|
|
|
|
if (isWeb) {
|
|
await triggerWebDownload(downloadUrl, projectForStage?.title)
|
|
await setLoading(false)
|
|
return
|
|
}
|
|
|
|
setIsDownloading(true)
|
|
try {
|
|
const baseName =
|
|
String(trackTitle || 'musicland-track')
|
|
.replace(/[\\/:*?"<>|]+/g, '-')
|
|
.trim() || 'musicland-track'
|
|
const fileName = `${baseName}.mp3`
|
|
const targetUri = `${FileSystem.cacheDirectory || ''}${fileName}`
|
|
|
|
const downloadResult = await FileSystem.downloadAsync(downloadUrl, targetUri)
|
|
if (!downloadResult?.uri) {
|
|
return
|
|
}
|
|
|
|
if (Platform.OS === 'android') {
|
|
const permissions =
|
|
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync()
|
|
if (!permissions.granted || !permissions.directoryUri) {
|
|
return
|
|
}
|
|
const base64 = await FileSystem.readAsStringAsync(downloadResult.uri, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
})
|
|
try {
|
|
const destUri = await FileSystem.StorageAccessFramework.createFileAsync(
|
|
permissions.directoryUri,
|
|
fileName,
|
|
'audio/mpeg'
|
|
)
|
|
await FileSystem.writeAsStringAsync(destUri, base64, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
})
|
|
} catch (error) {
|
|
console.log('[SongDownload] SAF write error', error?.message)
|
|
}
|
|
} else {
|
|
if (await Sharing.isAvailableAsync()) {
|
|
try {
|
|
await Sharing.shareAsync(downloadResult.uri, {
|
|
mimeType: 'audio/mpeg',
|
|
dialogTitle: musiclandShareHeading,
|
|
})
|
|
} catch (error) {
|
|
console.log('[SongDownload] share error', error?.message)
|
|
}
|
|
} else {
|
|
try {
|
|
await Linking.openURL(downloadResult.uri)
|
|
} catch (error) {
|
|
console.log('[SongDownload] open local file error', error?.message)
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('[SongDownload] native download open error', error?.message)
|
|
} finally {
|
|
setIsDownloading(false)
|
|
await setLoading(false)
|
|
}
|
|
}, [coverUrl, isDownloading, projectForStage, selectedOption, trackTitle])
|
|
|
|
const startSongDownloadCheckout = useCallback(
|
|
async ({ force = false } = {}) => {
|
|
if (!projectId) {
|
|
if (setTooltip) {
|
|
setTooltip({
|
|
type: 'error',
|
|
text: "Impossible d'identifier le projet pour le paiement.",
|
|
})
|
|
} else {
|
|
Alert.alert('Paiement', "Impossible d'identifier le projet pour le paiement.")
|
|
}
|
|
return
|
|
}
|
|
|
|
if (!force && (isCheckoutLaunching || downloadPaymentPending)) {
|
|
return
|
|
}
|
|
|
|
setIsCheckoutLaunching(true)
|
|
setDownloadPaymentPending(true)
|
|
try {
|
|
await createSongDownloadCheckout(projectId)
|
|
} catch (error) {
|
|
setDownloadPaymentPending(false)
|
|
if (setTooltip) {
|
|
setTooltip({
|
|
type: 'error',
|
|
text: error?.message || "Une erreur est survenue lors du paiement.",
|
|
})
|
|
} else {
|
|
Alert.alert('Paiement', error?.message || "Une erreur est survenue lors du paiement.")
|
|
}
|
|
} finally {
|
|
setIsCheckoutLaunching(false)
|
|
}
|
|
},
|
|
[
|
|
createSongDownloadCheckout,
|
|
downloadPaymentPending,
|
|
isCheckoutLaunching,
|
|
projectId,
|
|
setTooltip,
|
|
]
|
|
)
|
|
|
|
const handleDownloadPress = useCallback(() => {
|
|
if (isDownloading || isCheckoutLaunching) {
|
|
return
|
|
}
|
|
if (canDownloadDirectly) {
|
|
handleDownload()
|
|
return
|
|
}
|
|
if (downloadPaymentPending) {
|
|
Alert.alert(
|
|
'Paiement en attente',
|
|
"Le paiement n'est pas encore confirmé. Si tu as déjà payé, patiente quelques instants.",
|
|
[
|
|
{ text: 'Attendre', style: 'cancel' },
|
|
{ text: 'Relancer le paiement', onPress: () => startSongDownloadCheckout({ force: true }) },
|
|
]
|
|
)
|
|
return
|
|
}
|
|
if (promptPurchaseConfirm) {
|
|
Alert.alert(
|
|
'Télécharger ce morceau',
|
|
'Voulez-vous télécharger ce morceau pour 1,99€ ?',
|
|
[
|
|
{ text: 'Annuler', style: 'cancel' },
|
|
{ text: 'Acheter', onPress: () => startSongDownloadCheckout() },
|
|
],
|
|
{ cancelable: true }
|
|
)
|
|
return
|
|
}
|
|
startSongDownloadCheckout()
|
|
}, [
|
|
canDownloadDirectly,
|
|
downloadPaymentPending,
|
|
handleDownload,
|
|
isCheckoutLaunching,
|
|
isDownloading,
|
|
promptPurchaseConfirm,
|
|
startSongDownloadCheckout,
|
|
])
|
|
|
|
useEffect(() => {
|
|
if (!downloadPaymentPending || !hasPaidDownload || isDownloading) {
|
|
return
|
|
}
|
|
setDownloadPaymentPending(false)
|
|
handleDownload()
|
|
}, [downloadPaymentPending, handleDownload, hasPaidDownload, isDownloading])
|
|
|
|
const handleStopAdventure = useCallback(() => {
|
|
reopenAdventureModalOnFocusRef.current = false
|
|
setShowAdventureModal(false)
|
|
setAdventureChoice('stop')
|
|
}, [])
|
|
|
|
const handleGoHome = useCallback(() => {
|
|
navigate(Routes.BottomTab, {
|
|
screen: Routes.HomeStack,
|
|
params: {
|
|
screen: Routes.Home,
|
|
},
|
|
})
|
|
}, [])
|
|
|
|
return (
|
|
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
|
<MusicLandHeader onPressBack={handleBackPress} progress={84} />
|
|
{shouldShowDownloadPage ? (
|
|
<ScrollView
|
|
contentContainerStyle={[
|
|
styles.container,
|
|
!isWeb && styles.containerMobile,
|
|
styles.scrollContent,
|
|
]}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<CreateLyricsHeader title="Ta pochette est validée" />
|
|
<ClubAdvantagesCard
|
|
style={styles.clubCardSpacing}
|
|
topContent={
|
|
<View style={styles.coverRow}>
|
|
{coverUrl ? (
|
|
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
|
) : (
|
|
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
|
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.downloadColumn}>
|
|
<Pressable
|
|
style={[
|
|
styles.downloadTile,
|
|
(isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
|
]}
|
|
onPress={handleDownloadPress}
|
|
disabled={isDownloading || isCheckoutLaunching}
|
|
>
|
|
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
|
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
|
</Pressable>
|
|
{!canDownloadDirectly && downloadPaymentPending ? (
|
|
<Text style={styles.downloadPendingText}>
|
|
Paiement en attente de confirmation...
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
}
|
|
/>
|
|
<GradientButton
|
|
title="Revenir à l'accueil"
|
|
onPress={handleGoHome}
|
|
containerStyle={styles.returnHomeButton}
|
|
/>
|
|
</ScrollView>
|
|
) : (
|
|
<View style={styles.adventureGatePlaceholder} />
|
|
)}
|
|
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
|
|
<AdventureChoiceModal
|
|
isVisible={showAdventureModal}
|
|
setIsVisible={setShowAdventureModal}
|
|
onContinue={handleContinueAdventure}
|
|
onStop={handleStopAdventure}
|
|
/>
|
|
<ShareAndCreditsModal
|
|
isVisible={showShareModal}
|
|
setIsVisible={setShowShareModal}
|
|
onNavigateToCredits={() => {
|
|
setShowShareModal(false)
|
|
navigate(Routes.DownloadPrices, { action: 'song' })
|
|
}}
|
|
project={projectForStage}
|
|
selectedOption={selectedOption}
|
|
/>
|
|
</Page>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flexGrow: 1,
|
|
width: '100%',
|
|
paddingHorizontal: gutters * 1.2,
|
|
paddingBottom: gutters * 1.8,
|
|
paddingTop: gutters,
|
|
gap: gutters * 1.2,
|
|
},
|
|
containerMobile: {
|
|
paddingHorizontal: 0,
|
|
paddingBottom: gutters * 1.2,
|
|
paddingTop: gutters * 0.8,
|
|
},
|
|
scrollContent: {
|
|
paddingBottom: gutters * 2.6,
|
|
},
|
|
coverRow: {
|
|
flexDirection: isWeb ? 'row' : 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: gutters * 0.8,
|
|
},
|
|
coverImage: {
|
|
width: 150,
|
|
height: 150,
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.18)',
|
|
},
|
|
coverPlaceholder: {
|
|
...Style.centered,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.06)',
|
|
},
|
|
placeholderText: {
|
|
fontFamily: FONT_FAMILY.InterMedium,
|
|
color: Palette.grayMid,
|
|
},
|
|
downloadTile: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
paddingVertical: gutters * 0.9,
|
|
paddingHorizontal: gutters * 1.2,
|
|
borderRadius: 14,
|
|
backgroundColor: '#8C4BFF',
|
|
borderWidth: 0,
|
|
},
|
|
downloadColumn: {
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
downloadText: {
|
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
fontSize: 15,
|
|
color: Palette.white,
|
|
},
|
|
downloadPendingText: {
|
|
marginTop: 6,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
fontSize: 12,
|
|
color: Palette.white,
|
|
opacity: 0.8,
|
|
},
|
|
adventureGatePlaceholder: {
|
|
flex: 1,
|
|
},
|
|
clubCardSpacing: {
|
|
marginTop: gutters * 0.5,
|
|
},
|
|
returnHomeButton: {
|
|
width: '100%',
|
|
maxWidth: 320,
|
|
alignSelf: 'center',
|
|
marginTop: gutters * 0.6,
|
|
},
|
|
})
|
|
|
|
export default SongDownload
|