feat: cover
This commit is contained in:
+38
-20
@@ -32,40 +32,56 @@ const normalizeArtistName = (value) => {
|
||||
return trimmedValue || 'MusicLand'
|
||||
}
|
||||
|
||||
const getArtistFontSize = (artistName) => {
|
||||
if (artistName.length <= 16) return 52
|
||||
if (artistName.length <= 24) return 44
|
||||
const normalizeSongTitle = (value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return 'Sans titre'
|
||||
}
|
||||
const trimmedValue = value.trim()
|
||||
return trimmedValue || 'Sans titre'
|
||||
}
|
||||
|
||||
const getTitleFontSize = (songTitle) => {
|
||||
if (songTitle.length <= 16) return 38
|
||||
if (songTitle.length <= 24) return 32
|
||||
return 26
|
||||
}
|
||||
|
||||
const getUsernameFontSize = (username) => {
|
||||
if (username.length <= 16) return 52
|
||||
if (username.length <= 24) return 44
|
||||
return 36
|
||||
}
|
||||
|
||||
const buildTextOverlay = (artistName) => {
|
||||
const safeArtistName = escapeSvgText(artistName)
|
||||
const artistFontSize = getArtistFontSize(artistName)
|
||||
const buildTextOverlay = ({ songTitle, username }) => {
|
||||
const safeSongTitle = escapeSvgText(songTitle)
|
||||
const safeUsername = escapeSvgText(username)
|
||||
const titleFontSize = getTitleFontSize(songTitle)
|
||||
const usernameFontSize = getUsernameFontSize(username)
|
||||
|
||||
return Buffer.from(`
|
||||
<svg width="${COVER_SIZE}" height="${COVER_SIZE}" viewBox="0 0 ${COVER_SIZE} ${COVER_SIZE}" xmlns="http://www.w3.org/2000/svg">
|
||||
<text
|
||||
x="512"
|
||||
y="824"
|
||||
y="170"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-family="${FONT_FAMILY}"
|
||||
font-size="${artistFontSize}"
|
||||
font-size="${titleFontSize}"
|
||||
font-weight="400"
|
||||
letter-spacing="3"
|
||||
letter-spacing="2"
|
||||
fill="#FFFFFF"
|
||||
>${safeArtistName}</text>
|
||||
>${safeSongTitle}</text>
|
||||
<text
|
||||
x="512"
|
||||
y="963"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-family="${FONT_FAMILY}"
|
||||
font-size="54"
|
||||
font-size="${usernameFontSize}"
|
||||
font-weight="400"
|
||||
letter-spacing="5"
|
||||
letter-spacing="4"
|
||||
fill="#FFFFFF"
|
||||
>PLAYBACKER</text>
|
||||
>${safeUsername}</text>
|
||||
</svg>`)
|
||||
}
|
||||
|
||||
@@ -90,26 +106,28 @@ const buildCircularMask = (size) => {
|
||||
}
|
||||
|
||||
async function loadCoverIdentity(project) {
|
||||
const artistName = normalizeArtistName(project.userName)
|
||||
const username = normalizeArtistName(project.userName)
|
||||
const songTitle = normalizeSongTitle(project.title)
|
||||
const userId = typeof project.userId === 'string' ? project.userId.trim() : ''
|
||||
|
||||
if (!userId) {
|
||||
return { artistName, profilePictureURL: null }
|
||||
return { songTitle, username, profilePictureURL: project.coverCenterImageURL || null }
|
||||
}
|
||||
|
||||
try {
|
||||
const userSnapshot = await refList.users.doc(userId).get()
|
||||
const userData = userSnapshot.exists ? userSnapshot.data() : null
|
||||
return {
|
||||
artistName: normalizeArtistName(project.userName || userData?.userName),
|
||||
profilePictureURL: userData?.profilePictureURL || null,
|
||||
songTitle,
|
||||
username: normalizeArtistName(project.userName || userData?.userName),
|
||||
profilePictureURL: project.coverCenterImageURL || userData?.profilePictureURL || null,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[Cover] Unable to load profile picture', {
|
||||
projectId: project.id,
|
||||
error: error.message,
|
||||
})
|
||||
return { artistName, profilePictureURL: null }
|
||||
return { songTitle, username, profilePictureURL: project.coverCenterImageURL || null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,9 +187,9 @@ async function prepareMusiclandCover(project) {
|
||||
throw new Error('Utilisateur du projet introuvable')
|
||||
}
|
||||
|
||||
const { artistName, profilePictureURL } = await loadCoverIdentity(project)
|
||||
const { songTitle, username, profilePictureURL } = await loadCoverIdentity(project)
|
||||
const profileOverlay = await buildProfileOverlay(profilePictureURL)
|
||||
const overlays = [{ input: buildTextOverlay(artistName), left: 0, top: 0 }]
|
||||
const overlays = [{ input: buildTextOverlay({ songTitle, username }), left: 0, top: 0 }]
|
||||
|
||||
if (profileOverlay) {
|
||||
overlays.unshift({
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 488 KiB |
@@ -5,13 +5,16 @@ import { img } from '../assets'
|
||||
import { FONT_FAMILY } from '../styles/Fonts'
|
||||
|
||||
const MusiclandPlaybackCover = ({
|
||||
artistName,
|
||||
songTitle,
|
||||
username,
|
||||
profilePictureURL,
|
||||
coverUrl,
|
||||
style,
|
||||
}) => {
|
||||
const displayName =
|
||||
typeof artistName === 'string' && artistName.trim() ? artistName.trim() : 'MusicLand'
|
||||
const displayTitle =
|
||||
typeof songTitle === 'string' && songTitle.trim() ? songTitle.trim() : 'Sans titre'
|
||||
const displayUsername =
|
||||
typeof username === 'string' && username.trim() ? username.trim() : 'MusicLand'
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
@@ -36,11 +39,18 @@ const MusiclandPlaybackCover = ({
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.55}
|
||||
numberOfLines={1}
|
||||
style={styles.artistName}
|
||||
style={styles.songTitle}
|
||||
>
|
||||
{displayName}
|
||||
{displayTitle}
|
||||
</Text>
|
||||
<Text
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.55}
|
||||
numberOfLines={1}
|
||||
style={styles.username}
|
||||
>
|
||||
{displayUsername}
|
||||
</Text>
|
||||
<Text style={styles.playbacker}>PLAYBACKER</Text>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -63,19 +73,19 @@ const styles = StyleSheet.create({
|
||||
top: '39.84375%',
|
||||
borderRadius: 999,
|
||||
},
|
||||
artistName: {
|
||||
songTitle: {
|
||||
position: 'absolute',
|
||||
top: '76.5%',
|
||||
left: '16%',
|
||||
width: '68%',
|
||||
top: '15%',
|
||||
left: '10%',
|
||||
width: '80%',
|
||||
color: '#FFFFFF',
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 21,
|
||||
lineHeight: 26,
|
||||
letterSpacing: 1.2,
|
||||
fontSize: 14,
|
||||
lineHeight: 18,
|
||||
letterSpacing: 0.8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
playbacker: {
|
||||
username: {
|
||||
position: 'absolute',
|
||||
bottom: '3%',
|
||||
left: '10%',
|
||||
|
||||
@@ -330,7 +330,8 @@ export const ChooseCoverType = () => {
|
||||
</View>
|
||||
<View style={styles.previewContainer}>
|
||||
<MusiclandPlaybackCover
|
||||
artistName={selectedProject?.userName || currentUserData?.userName || 'MusicLand'}
|
||||
songTitle={selectedProject?.title}
|
||||
username={selectedProject?.userName || currentUserData?.userName || 'MusicLand'}
|
||||
profilePictureURL={
|
||||
selectedProject?.coverCenterImageURL || currentUserData?.profilePictureURL || null
|
||||
}
|
||||
|
||||
@@ -35,10 +35,11 @@ const PouchReady = () => {
|
||||
const coverUrl = templateCover?.result || null
|
||||
const isPreparing = selectedProject?.coverStatus === 'GENERATING'
|
||||
const hasPreparationError = selectedProject?.coverStatus === 'ERROR'
|
||||
const artistName =
|
||||
const username =
|
||||
typeof selectedProject?.userName === 'string' && selectedProject.userName.trim()
|
||||
? selectedProject.userName.trim()
|
||||
: currentUserData?.userName || 'MusicLand'
|
||||
const songTitle = selectedProject?.title || 'Sans titre'
|
||||
const profilePictureURL = currentUserData?.profilePictureURL || null
|
||||
const centerImageURL = selectedProject?.coverCenterImageURL || profilePictureURL
|
||||
|
||||
@@ -119,7 +120,8 @@ const PouchReady = () => {
|
||||
throw new Error('cover_project_identity_missing')
|
||||
}
|
||||
const coverBlob = await createMusiclandPlaybackCover({
|
||||
artistName,
|
||||
songTitle,
|
||||
username,
|
||||
profilePictureURL: centerImageURL,
|
||||
})
|
||||
const storagePath = `users/${userId}/projects/${selectedProjectId}/cover-musicland-playbacker.png`
|
||||
@@ -170,13 +172,14 @@ const PouchReady = () => {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, [
|
||||
artistName,
|
||||
coverUrl,
|
||||
centerImageURL,
|
||||
selectedOption,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
songTitle,
|
||||
updateProjectData,
|
||||
username,
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -193,7 +196,8 @@ const PouchReady = () => {
|
||||
/>
|
||||
<View style={[styles.previewArea, Style.containerCenter]}>
|
||||
<MusiclandPlaybackCover
|
||||
artistName={artistName}
|
||||
songTitle={songTitle}
|
||||
username={username}
|
||||
profilePictureURL={centerImageURL}
|
||||
coverUrl={coverUrl}
|
||||
style={isWeb ? styles.coverWeb : styles.coverMobile}
|
||||
|
||||
@@ -28,9 +28,15 @@ const drawCenteredText = (context, text, y, fontSize, letterSpacing) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getArtistFontSize = (artistName) => {
|
||||
if (artistName.length <= 16) return 52
|
||||
if (artistName.length <= 24) return 44
|
||||
const getTitleFontSize = (songTitle) => {
|
||||
if (songTitle.length <= 16) return 38
|
||||
if (songTitle.length <= 24) return 32
|
||||
return 26
|
||||
}
|
||||
|
||||
const getUsernameFontSize = (username) => {
|
||||
if (username.length <= 16) return 52
|
||||
if (username.length <= 24) return 44
|
||||
return 36
|
||||
}
|
||||
|
||||
@@ -67,7 +73,7 @@ const drawProfilePicture = (context, profilePicture) => {
|
||||
context.restore()
|
||||
}
|
||||
|
||||
export const createMusiclandPlaybackCover = async ({ artistName, profilePictureURL }) => {
|
||||
export const createMusiclandPlaybackCover = async ({ songTitle, username, profilePictureURL }) => {
|
||||
const templateAsset = Asset.fromModule(img.musiclandPlaybackCover)
|
||||
await templateAsset.downloadAsync()
|
||||
const assetUri = templateAsset.localUri || templateAsset.uri
|
||||
@@ -91,10 +97,12 @@ export const createMusiclandPlaybackCover = async ({ artistName, profilePictureU
|
||||
|
||||
context.fillStyle = '#FFFFFF'
|
||||
context.textBaseline = 'middle'
|
||||
context.font = `400 ${getArtistFontSize(artistName)}px Inter, Arial, sans-serif`
|
||||
drawCenteredText(context, artistName, 824, getArtistFontSize(artistName), 3)
|
||||
context.font = '400 54px Inter, Arial, sans-serif'
|
||||
drawCenteredText(context, 'PLAYBACKER', 963, 54, 5)
|
||||
const titleFontSize = getTitleFontSize(songTitle)
|
||||
context.font = `400 ${titleFontSize}px Inter, Arial, sans-serif`
|
||||
drawCenteredText(context, songTitle, 170, titleFontSize, 2)
|
||||
const usernameFontSize = getUsernameFontSize(username)
|
||||
context.font = `400 ${usernameFontSize}px Inter, Arial, sans-serif`
|
||||
drawCenteredText(context, username, 963, usernameFontSize, 4)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob((blob) => {
|
||||
|
||||
Reference in New Issue
Block a user