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'
}
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

+24 -14
View File
@@ -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%',
+2 -1
View File
@@ -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
}
+8 -4
View File
@@ -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}
+16 -8
View File
@@ -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) => {