feat: cover

This commit is contained in:
leon
2026-09-09 12:06:17 +02:00
parent 87d82147a3
commit 826748075c
6 changed files with 88 additions and 47 deletions
+38 -20
View File
@@ -32,40 +32,56 @@ const normalizeArtistName = (value) => {
return trimmedValue || 'MusicLand' return trimmedValue || 'MusicLand'
} }
const getArtistFontSize = (artistName) => { const normalizeSongTitle = (value) => {
if (artistName.length <= 16) return 52 if (typeof value !== 'string') {
if (artistName.length <= 24) return 44 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 return 36
} }
const buildTextOverlay = (artistName) => { const buildTextOverlay = ({ songTitle, username }) => {
const safeArtistName = escapeSvgText(artistName) const safeSongTitle = escapeSvgText(songTitle)
const artistFontSize = getArtistFontSize(artistName) const safeUsername = escapeSvgText(username)
const titleFontSize = getTitleFontSize(songTitle)
const usernameFontSize = getUsernameFontSize(username)
return Buffer.from(` return Buffer.from(`
<svg width="${COVER_SIZE}" height="${COVER_SIZE}" viewBox="0 0 ${COVER_SIZE} ${COVER_SIZE}" xmlns="http://www.w3.org/2000/svg"> <svg width="${COVER_SIZE}" height="${COVER_SIZE}" viewBox="0 0 ${COVER_SIZE} ${COVER_SIZE}" xmlns="http://www.w3.org/2000/svg">
<text <text
x="512" x="512"
y="824" y="170"
text-anchor="middle" text-anchor="middle"
dominant-baseline="middle" dominant-baseline="middle"
font-family="${FONT_FAMILY}" font-family="${FONT_FAMILY}"
font-size="${artistFontSize}" font-size="${titleFontSize}"
font-weight="400" font-weight="400"
letter-spacing="3" letter-spacing="2"
fill="#FFFFFF" fill="#FFFFFF"
>${safeArtistName}</text> >${safeSongTitle}</text>
<text <text
x="512" x="512"
y="963" y="963"
text-anchor="middle" text-anchor="middle"
dominant-baseline="middle" dominant-baseline="middle"
font-family="${FONT_FAMILY}" font-family="${FONT_FAMILY}"
font-size="54" font-size="${usernameFontSize}"
font-weight="400" font-weight="400"
letter-spacing="5" letter-spacing="4"
fill="#FFFFFF" fill="#FFFFFF"
>PLAYBACKER</text> >${safeUsername}</text>
</svg>`) </svg>`)
} }
@@ -90,26 +106,28 @@ const buildCircularMask = (size) => {
} }
async function loadCoverIdentity(project) { 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() : '' const userId = typeof project.userId === 'string' ? project.userId.trim() : ''
if (!userId) { if (!userId) {
return { artistName, profilePictureURL: null } return { songTitle, username, profilePictureURL: project.coverCenterImageURL || null }
} }
try { try {
const userSnapshot = await refList.users.doc(userId).get() const userSnapshot = await refList.users.doc(userId).get()
const userData = userSnapshot.exists ? userSnapshot.data() : null const userData = userSnapshot.exists ? userSnapshot.data() : null
return { return {
artistName: normalizeArtistName(project.userName || userData?.userName), songTitle,
profilePictureURL: userData?.profilePictureURL || null, username: normalizeArtistName(project.userName || userData?.userName),
profilePictureURL: project.coverCenterImageURL || userData?.profilePictureURL || null,
} }
} catch (error) { } catch (error) {
logger.warn('[Cover] Unable to load profile picture', { logger.warn('[Cover] Unable to load profile picture', {
projectId: project.id, projectId: project.id,
error: error.message, 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') 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 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) { if (profileOverlay) {
overlays.unshift({ overlays.unshift({
Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 488 KiB

+24 -14
View File
@@ -5,13 +5,16 @@ import { img } from '../assets'
import { FONT_FAMILY } from '../styles/Fonts' import { FONT_FAMILY } from '../styles/Fonts'
const MusiclandPlaybackCover = ({ const MusiclandPlaybackCover = ({
artistName, songTitle,
username,
profilePictureURL, profilePictureURL,
coverUrl, coverUrl,
style, style,
}) => { }) => {
const displayName = const displayTitle =
typeof artistName === 'string' && artistName.trim() ? artistName.trim() : 'MusicLand' typeof songTitle === 'string' && songTitle.trim() ? songTitle.trim() : 'Sans titre'
const displayUsername =
typeof username === 'string' && username.trim() ? username.trim() : 'MusicLand'
return ( return (
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
@@ -36,11 +39,18 @@ const MusiclandPlaybackCover = ({
adjustsFontSizeToFit adjustsFontSizeToFit
minimumFontScale={0.55} minimumFontScale={0.55}
numberOfLines={1} numberOfLines={1}
style={styles.artistName} style={styles.songTitle}
> >
{displayName} {displayTitle}
</Text>
<Text
adjustsFontSizeToFit
minimumFontScale={0.55}
numberOfLines={1}
style={styles.username}
>
{displayUsername}
</Text> </Text>
<Text style={styles.playbacker}>PLAYBACKER</Text>
</> </>
) : null} ) : null}
</View> </View>
@@ -63,19 +73,19 @@ const styles = StyleSheet.create({
top: '39.84375%', top: '39.84375%',
borderRadius: 999, borderRadius: 999,
}, },
artistName: { songTitle: {
position: 'absolute', position: 'absolute',
top: '76.5%', top: '15%',
left: '16%', left: '10%',
width: '68%', width: '80%',
color: '#FFFFFF', color: '#FFFFFF',
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
fontSize: 21, fontSize: 14,
lineHeight: 26, lineHeight: 18,
letterSpacing: 1.2, letterSpacing: 0.8,
textAlign: 'center', textAlign: 'center',
}, },
playbacker: { username: {
position: 'absolute', position: 'absolute',
bottom: '3%', bottom: '3%',
left: '10%', left: '10%',
+2 -1
View File
@@ -330,7 +330,8 @@ export const ChooseCoverType = () => {
</View> </View>
<View style={styles.previewContainer}> <View style={styles.previewContainer}>
<MusiclandPlaybackCover <MusiclandPlaybackCover
artistName={selectedProject?.userName || currentUserData?.userName || 'MusicLand'} songTitle={selectedProject?.title}
username={selectedProject?.userName || currentUserData?.userName || 'MusicLand'}
profilePictureURL={ profilePictureURL={
selectedProject?.coverCenterImageURL || currentUserData?.profilePictureURL || null selectedProject?.coverCenterImageURL || currentUserData?.profilePictureURL || null
} }
+8 -4
View File
@@ -35,10 +35,11 @@ const PouchReady = () => {
const coverUrl = templateCover?.result || null const coverUrl = templateCover?.result || null
const isPreparing = selectedProject?.coverStatus === 'GENERATING' const isPreparing = selectedProject?.coverStatus === 'GENERATING'
const hasPreparationError = selectedProject?.coverStatus === 'ERROR' const hasPreparationError = selectedProject?.coverStatus === 'ERROR'
const artistName = const username =
typeof selectedProject?.userName === 'string' && selectedProject.userName.trim() typeof selectedProject?.userName === 'string' && selectedProject.userName.trim()
? selectedProject.userName.trim() ? selectedProject.userName.trim()
: currentUserData?.userName || 'MusicLand' : currentUserData?.userName || 'MusicLand'
const songTitle = selectedProject?.title || 'Sans titre'
const profilePictureURL = currentUserData?.profilePictureURL || null const profilePictureURL = currentUserData?.profilePictureURL || null
const centerImageURL = selectedProject?.coverCenterImageURL || profilePictureURL const centerImageURL = selectedProject?.coverCenterImageURL || profilePictureURL
@@ -119,7 +120,8 @@ const PouchReady = () => {
throw new Error('cover_project_identity_missing') throw new Error('cover_project_identity_missing')
} }
const coverBlob = await createMusiclandPlaybackCover({ const coverBlob = await createMusiclandPlaybackCover({
artistName, songTitle,
username,
profilePictureURL: centerImageURL, profilePictureURL: centerImageURL,
}) })
const storagePath = `users/${userId}/projects/${selectedProjectId}/cover-musicland-playbacker.png` const storagePath = `users/${userId}/projects/${selectedProjectId}/cover-musicland-playbacker.png`
@@ -170,13 +172,14 @@ const PouchReady = () => {
setIsSaving(false) setIsSaving(false)
} }
}, [ }, [
artistName,
coverUrl, coverUrl,
centerImageURL, centerImageURL,
selectedOption, selectedOption,
selectedProject, selectedProject,
selectedProjectId, selectedProjectId,
songTitle,
updateProjectData, updateProjectData,
username,
]) ])
return ( return (
@@ -193,7 +196,8 @@ const PouchReady = () => {
/> />
<View style={[styles.previewArea, Style.containerCenter]}> <View style={[styles.previewArea, Style.containerCenter]}>
<MusiclandPlaybackCover <MusiclandPlaybackCover
artistName={artistName} songTitle={songTitle}
username={username}
profilePictureURL={centerImageURL} profilePictureURL={centerImageURL}
coverUrl={coverUrl} coverUrl={coverUrl}
style={isWeb ? styles.coverWeb : styles.coverMobile} style={isWeb ? styles.coverWeb : styles.coverMobile}
+16 -8
View File
@@ -28,9 +28,15 @@ const drawCenteredText = (context, text, y, fontSize, letterSpacing) => {
}) })
} }
const getArtistFontSize = (artistName) => { const getTitleFontSize = (songTitle) => {
if (artistName.length <= 16) return 52 if (songTitle.length <= 16) return 38
if (artistName.length <= 24) return 44 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 return 36
} }
@@ -67,7 +73,7 @@ const drawProfilePicture = (context, profilePicture) => {
context.restore() context.restore()
} }
export const createMusiclandPlaybackCover = async ({ artistName, profilePictureURL }) => { export const createMusiclandPlaybackCover = async ({ songTitle, username, profilePictureURL }) => {
const templateAsset = Asset.fromModule(img.musiclandPlaybackCover) const templateAsset = Asset.fromModule(img.musiclandPlaybackCover)
await templateAsset.downloadAsync() await templateAsset.downloadAsync()
const assetUri = templateAsset.localUri || templateAsset.uri const assetUri = templateAsset.localUri || templateAsset.uri
@@ -91,10 +97,12 @@ export const createMusiclandPlaybackCover = async ({ artistName, profilePictureU
context.fillStyle = '#FFFFFF' context.fillStyle = '#FFFFFF'
context.textBaseline = 'middle' context.textBaseline = 'middle'
context.font = `400 ${getArtistFontSize(artistName)}px Inter, Arial, sans-serif` const titleFontSize = getTitleFontSize(songTitle)
drawCenteredText(context, artistName, 824, getArtistFontSize(artistName), 3) context.font = `400 ${titleFontSize}px Inter, Arial, sans-serif`
context.font = '400 54px Inter, Arial, sans-serif' drawCenteredText(context, songTitle, 170, titleFontSize, 2)
drawCenteredText(context, 'PLAYBACKER', 963, 54, 5) const usernameFontSize = getUsernameFontSize(username)
context.font = `400 ${usernameFontSize}px Inter, Arial, sans-serif`
drawCenteredText(context, username, 963, usernameFontSize, 4)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
canvas.toBlob((blob) => { canvas.toBlob((blob) => {