feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+22 -22
View File
@@ -1,24 +1,24 @@
import { registerSheet } from "react-native-actions-sheet";
import DeleteModal from "../components/modal/DeleteModal";
import ProfileSettingsModal from "../components/modal/ProfileSettingsModal";
import DeleteAccountModal from "../components/modal/DeleteAccountModal";
import DeletePlaybackModal from "../components/modal/DeletePlaybackModal";
import DeleteAudioModal from "../components/modal/DeleteAudioModal";
import PlaylistModal from "../components/modal/PlaylistModal";
import PlaybackPickerModal from "../components/modal/PlaybackPickerModal";
import ShareModal from "../components/modal/ShareModal";
import ReportModal from "../components/modal/ReportModal";
import MusicOptionsModal from "../components/modal/MusicOptionsModal";
import { registerSheet } from 'react-native-actions-sheet'
import DeleteModal from '../components/modal/DeleteModal'
import ProfileSettingsModal from '../components/modal/ProfileSettingsModal'
import DeleteAccountModal from '../components/modal/DeleteAccountModal'
import DeletePlaybackModal from '../components/modal/DeletePlaybackModal'
import DeleteAudioModal from '../components/modal/DeleteAudioModal'
import PlaylistModal from '../components/modal/PlaylistModal'
import PlaybackPickerModal from '../components/modal/PlaybackPickerModal'
import ShareModal from '../components/modal/ShareModal'
import ReportModal from '../components/modal/ReportModal'
import MusicOptionsModal from '../components/modal/MusicOptionsModal'
registerSheet("Delete", DeleteModal);
registerSheet("ProfileSettings", ProfileSettingsModal);
registerSheet("DeleteAccount", DeleteAccountModal);
registerSheet("DeletePlayback", DeletePlaybackModal);
registerSheet("DeleteAudio", DeleteAudioModal);
registerSheet("Playlist", PlaylistModal);
registerSheet("PlaybackPicker", PlaybackPickerModal);
registerSheet("Share", ShareModal);
registerSheet("Report", ReportModal);
registerSheet("MusicOptions", MusicOptionsModal);
registerSheet('Delete', DeleteModal)
registerSheet('ProfileSettings', ProfileSettingsModal)
registerSheet('DeleteAccount', DeleteAccountModal)
registerSheet('DeletePlayback', DeletePlaybackModal)
registerSheet('DeleteAudio', DeleteAudioModal)
registerSheet('Playlist', PlaylistModal)
registerSheet('PlaybackPicker', PlaybackPickerModal)
registerSheet('Share', ShareModal)
registerSheet('Report', ReportModal)
registerSheet('MusicOptions', MusicOptionsModal)
export {};
export {}
+18 -27
View File
@@ -1,39 +1,30 @@
export const buildFullName = ({ firstName, lastName, displayName } = {}) => {
const first =
typeof firstName === "string" && firstName.trim().length > 0
? firstName.trim()
: "";
const last =
typeof lastName === "string" && lastName.trim().length > 0
? lastName.trim()
: "";
const nameFromParts = [first, last].filter(Boolean).join(" ").trim();
if (nameFromParts) return nameFromParts;
const first = typeof firstName === 'string' && firstName.trim().length > 0 ? firstName.trim() : ''
const last = typeof lastName === 'string' && lastName.trim().length > 0 ? lastName.trim() : ''
const nameFromParts = [first, last].filter(Boolean).join(' ').trim()
if (nameFromParts) return nameFromParts
const fallbackDisplay =
typeof displayName === "string" && displayName.trim().length > 0
? displayName.trim()
: "";
return fallbackDisplay || null;
};
typeof displayName === 'string' && displayName.trim().length > 0 ? displayName.trim() : ''
return fallbackDisplay || null
}
export const getArtistDisplayName = (source, fallback = "") => {
if (!source) return fallback;
const rawUserName =
typeof source?.userName === "string" ? source.userName.trim() : "";
if (rawUserName) return rawUserName;
export const getArtistDisplayName = (source, fallback = '') => {
if (!source) return fallback
const rawUserName = typeof source?.userName === 'string' ? source.userName.trim() : ''
if (rawUserName) return rawUserName
const nameFromParts = buildFullName({
firstName: source?.firstName,
lastName: source?.lastName,
displayName: source?.displayName,
});
})
if (nameFromParts) return nameFromParts;
return fallback;
};
if (nameFromParts) return nameFromParts
return fallback
}
export const getUserPreferredArtistName = (user) => {
const display = getArtistDisplayName(user, "");
return display || null;
};
const display = getArtistDisplayName(user, '')
return display || null
}
+20 -20
View File
@@ -1,40 +1,40 @@
import { getGlobal } from "reactn";
import { navigate } from "../navigation/NavigationService";
import { Routes } from "../navigation/Routes";
import { getGlobal } from 'reactn'
import { navigate } from '../navigation/NavigationService'
import { Routes } from '../navigation/Routes'
const redirectToRegister = () => {
try {
const activeRoute = getGlobal()?.activeRouteName;
const activeRoute = getGlobal()?.activeRouteName
if (activeRoute === Routes.Register) {
return;
return
}
} catch (error) {
console.warn("authRedirect: unable to read active route", error);
console.warn('authRedirect: unable to read active route', error)
}
navigate(Routes.Register);
};
navigate(Routes.Register)
}
export const ensureAuthenticated = (currentUID, options = {}) => {
const { onIntercept } = options || {};
const { onIntercept } = options || {}
if (currentUID) {
return true;
return true
}
try {
if (typeof onIntercept === "function") {
onIntercept();
if (typeof onIntercept === 'function') {
onIntercept()
}
} catch (error) {
console.warn("authRedirect: onIntercept error", error);
console.warn('authRedirect: onIntercept error', error)
}
redirectToRegister();
return false;
};
redirectToRegister()
return false
}
export const withAuthGuard = async (currentUID, handler, options = {}) => {
if (!ensureAuthenticated(currentUID, options)) {
return;
return
}
return handler?.();
};
return handler?.()
}
export default ensureAuthenticated;
export default ensureAuthenticated
+23 -23
View File
@@ -1,48 +1,48 @@
const globalScope =
typeof globalThis !== "undefined"
typeof globalThis !== 'undefined'
? globalThis
: typeof global !== "undefined"
: typeof global !== 'undefined'
? global
: typeof window !== "undefined"
: typeof window !== 'undefined'
? window
: undefined;
: undefined
const hasObjectUrlSupport = Boolean(
globalScope?.URL && typeof globalScope.URL.createObjectURL === "function"
);
globalScope?.URL && typeof globalScope.URL.createObjectURL === 'function'
)
const REGISTRY_KEY = "__musiclandBlobRegistry";
const REGISTRY_KEY = '__musiclandBlobRegistry'
const blobRegistry = (() => {
if (globalScope && globalScope[REGISTRY_KEY] instanceof Map) {
return globalScope[REGISTRY_KEY];
return globalScope[REGISTRY_KEY]
}
const registry = new Map();
const registry = new Map()
if (globalScope) {
try {
globalScope[REGISTRY_KEY] = registry;
globalScope[REGISTRY_KEY] = registry
} catch {}
}
return registry;
})();
return registry
})()
const isValidBlobUrl = (url) =>
hasObjectUrlSupport && typeof url === "string" && url.startsWith("blob:");
hasObjectUrlSupport && typeof url === 'string' && url.startsWith('blob:')
export const registerBlobUrl = (url, blob) => {
if (!isValidBlobUrl(url) || !blob) return;
blobRegistry.set(url, blob);
};
if (!isValidBlobUrl(url) || !blob) return
blobRegistry.set(url, blob)
}
export const getBlobForUrl = (url) => {
if (!isValidBlobUrl(url)) return null;
return blobRegistry.get(url) || null;
};
if (!isValidBlobUrl(url)) return null
return blobRegistry.get(url) || null
}
export const releaseBlobUrl = (url) => {
if (!isValidBlobUrl(url)) return;
blobRegistry.delete(url);
if (!isValidBlobUrl(url)) return
blobRegistry.delete(url)
try {
globalScope.URL.revokeObjectURL(url);
globalScope.URL.revokeObjectURL(url)
} catch {}
};
}
+11 -11
View File
@@ -1,21 +1,21 @@
const listeners = new Set();
const listeners = new Set()
export const subscribeCoinPackModal = (listener) => {
if (typeof listener !== "function") {
return () => {};
if (typeof listener !== 'function') {
return () => {}
}
listeners.add(listener);
listeners.add(listener)
return () => {
listeners.delete(listener);
};
};
listeners.delete(listener)
}
}
export const openCoinPackModal = () => {
listeners.forEach((listener) => {
try {
listener();
listener()
} catch (error) {
console.error("[coinPackModal] open listener error", error);
console.error('[coinPackModal] open listener error', error)
}
});
};
})
}
+23 -23
View File
@@ -1,44 +1,44 @@
const toDate = (value) => {
if (!value) {
return null;
return null
}
if (typeof value.toDate === "function") {
if (typeof value.toDate === 'function') {
try {
return value.toDate();
return value.toDate()
} catch (_error) {
return null;
return null
}
}
if (value instanceof Date) {
return value;
return value
}
if (typeof value === "number" && Number.isFinite(value)) {
if (typeof value === 'number' && Number.isFinite(value)) {
if (value > 1e12) {
return new Date(value);
return new Date(value)
}
return new Date(value * 1000);
return new Date(value * 1000)
}
if (typeof value === "object" && Number.isFinite(value.seconds)) {
return new Date(value.seconds * 1000);
if (typeof value === 'object' && Number.isFinite(value.seconds)) {
return new Date(value.seconds * 1000)
}
return null;
};
return null
}
const formatDate = (date) => {
if (!(date instanceof Date)) {
return "À déterminer";
return 'À déterminer'
}
try {
return new Intl.DateTimeFormat("fr-FR", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
} catch (_error) {
return date.toString();
return date.toString()
}
};
}
export { toDate, formatDate };
export { toDate, formatDate }
+18 -22
View File
@@ -1,37 +1,33 @@
import QRCode from "qrcode-terminal/vendor/QRCode";
import QRErrorCorrectLevel from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel";
import QRCode from 'qrcode-terminal/vendor/QRCode'
import QRErrorCorrectLevel from 'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel'
const getErrorLevel = (level) => {
if (typeof level === "number") {
return level;
if (typeof level === 'number') {
return level
}
const normalized = String(level || "M").toUpperCase();
return QRErrorCorrectLevel?.[normalized] ?? QRErrorCorrectLevel.M;
};
const normalized = String(level || 'M').toUpperCase()
return QRErrorCorrectLevel?.[normalized] ?? QRErrorCorrectLevel.M
}
export const generateQrMatrix = ({
value,
errorCorrectionLevel = "M",
}) => {
export const generateQrMatrix = ({ value, errorCorrectionLevel = 'M' }) => {
if (!value) {
throw new Error("generateQrMatrix requires a value");
throw new Error('generateQrMatrix requires a value')
}
const qr = new QRCode(-1, getErrorLevel(errorCorrectionLevel));
qr.addData(value);
qr.make();
const qr = new QRCode(-1, getErrorLevel(errorCorrectionLevel))
qr.addData(value)
qr.make()
const size = qr.getModuleCount();
const matrix = new Array(size);
const size = qr.getModuleCount()
const matrix = new Array(size)
for (let row = 0; row < size; row += 1) {
matrix[row] = new Array(size);
matrix[row] = new Array(size)
for (let col = 0; col < size; col += 1) {
matrix[row][col] = qr.isDark(row, col);
matrix[row][col] = qr.isDark(row, col)
}
}
return { matrix, size };
};
return { matrix, size }
}
+25 -39
View File
@@ -1,56 +1,42 @@
import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
import updateLocale from 'dayjs/plugin/updateLocale';
import relativeTime from 'dayjs/plugin/relativeTime';
import reactotron from 'reactotron-react-native';
import dayjs from 'dayjs'
import isToday from 'dayjs/plugin/isToday'
import isYesterday from 'dayjs/plugin/isYesterday'
import updateLocale from 'dayjs/plugin/updateLocale'
import relativeTime from 'dayjs/plugin/relativeTime'
import reactotron from 'reactotron-react-native'
// Dayjs plugins
dayjs.extend(isToday);
dayjs.extend(isYesterday);
dayjs.extend(updateLocale);
dayjs.extend(relativeTime);
dayjs.extend(isToday)
dayjs.extend(isYesterday)
dayjs.extend(updateLocale)
dayjs.extend(relativeTime)
/**
* Return the distance between the given date and now in words.
*/
export const distanceToNow = value => {
export const distanceToNow = (value) => {
if (!dayjs(value).isValid()) {
return 'Invalid Date';
return 'Invalid Date'
}
return dayjs(value).fromNow();
};
return dayjs(value).fromNow()
}
export const warn = reactotron.warn;
export const warn = reactotron.warn
export function decode(t, e) {
for (
var n,
o,
u = 0,
l = 0,
r = 0,
d = [],
h = 0,
i = 0,
a = null,
c = Math.pow(10, e || 5);
var n, o, u = 0, l = 0, r = 0, d = [], h = 0, i = 0, a = null, c = Math.pow(10, e || 5);
u < t.length;
) {
(a = null), (h = 0), (i = 0);
do (a = t.charCodeAt(u++) - 63), (i |= (31 & a) << h), (h += 5);
while (a >= 32);
(n = 1 & i ? ~(i >> 1) : i >> 1), (h = i = 0);
do (a = t.charCodeAt(u++) - 63), (i |= (31 & a) << h), (h += 5);
while (a >= 32);
(o = 1 & i ? ~(i >> 1) : i >> 1),
(l += n),
(r += o),
d.push([l / c, r / c]);
;((a = null), (h = 0), (i = 0))
do ((a = t.charCodeAt(u++) - 63), (i |= (31 & a) << h), (h += 5))
while (a >= 32)
;((n = 1 & i ? ~(i >> 1) : i >> 1), (h = i = 0))
do ((a = t.charCodeAt(u++) - 63), (i |= (31 & a) << h), (h += 5))
while (a >= 32)
;((o = 1 & i ? ~(i >> 1) : i >> 1), (l += n), (r += o), d.push([l / c, r / c]))
}
return d.map(function (t) {
return {latitude: t[0], longitude: t[1]};
});
return { latitude: t[0], longitude: t[1] }
})
}
+43 -48
View File
@@ -4,55 +4,51 @@ import {
deleteField,
projectsRef,
serverTimestamp,
} from "../config/firebase";
import { ensureAuthenticated } from "./authRedirect";
} from '../config/firebase'
import { ensureAuthenticated } from './authRedirect'
export const LIKE_TARGET = {
SONG: "song",
PLAYBACK: "playback",
};
SONG: 'song',
PLAYBACK: 'playback',
}
export const getLikeFieldPath = (target = LIKE_TARGET.SONG) => {
return target === LIKE_TARGET.PLAYBACK ? "likes.playback" : "likes.song";
};
return target === LIKE_TARGET.PLAYBACK ? 'likes.playback' : 'likes.song'
}
const getLikeTimestampPath = (target = LIKE_TARGET.SONG) => {
if (target === LIKE_TARGET.PLAYBACK) {
return "likes.playbackLikedAt";
return 'likes.playbackLikedAt'
}
if (target === LIKE_TARGET.SONG) {
return "likes.songLikedAt";
return 'likes.songLikedAt'
}
return null;
};
return null
}
export const getProjectLikes = (project, target = LIKE_TARGET.SONG) => {
const path = target === LIKE_TARGET.PLAYBACK ? "playback" : "song";
const likes = project?.likes;
const list = likes ? likes[path] : null;
const normalized = Array.isArray(list) ? list : [];
const legacy = Array.isArray(project?.likedBy) ? project.likedBy : [];
const path = target === LIKE_TARGET.PLAYBACK ? 'playback' : 'song'
const likes = project?.likes
const list = likes ? likes[path] : null
const normalized = Array.isArray(list) ? list : []
const legacy = Array.isArray(project?.likedBy) ? project.likedBy : []
if (target !== LIKE_TARGET.SONG || legacy.length === 0) {
return normalized;
return normalized
}
const merged = new Set(normalized);
const merged = new Set(normalized)
legacy.forEach((uid) => {
if (uid) merged.add(uid);
});
if (uid) merged.add(uid)
})
return Array.from(merged);
};
return Array.from(merged)
}
export const isProjectLikedByUser = (
project,
target,
currentUID
) => {
if (!currentUID) return false;
return getProjectLikes(project, target).includes(currentUID);
};
export const isProjectLikedByUser = (project, target, currentUID) => {
if (!currentUID) return false
return getProjectLikes(project, target).includes(currentUID)
}
export const toggleProjectLike = async ({
projectId,
@@ -60,39 +56,38 @@ export const toggleProjectLike = async ({
currentUID,
next,
}) => {
if (!projectId) return;
if (!ensureAuthenticated(currentUID)) return;
const likeFieldKey =
target === LIKE_TARGET.PLAYBACK ? "playback" : "song";
const likeOperation = next ? arrayUnion(currentUID) : arrayRemove(currentUID);
if (!projectId) return
if (!ensureAuthenticated(currentUID)) return
const likeFieldKey = target === LIKE_TARGET.PLAYBACK ? 'playback' : 'song'
const likeOperation = next ? arrayUnion(currentUID) : arrayRemove(currentUID)
const likedAtKey =
target === LIKE_TARGET.PLAYBACK
? "playbackLikedAt"
? 'playbackLikedAt'
: target === LIKE_TARGET.SONG
? "songLikedAt"
: null;
? 'songLikedAt'
: null
const likesPayload = {
likes: {
[likeFieldKey]: likeOperation,
...(likedAtKey && currentUID
? {
[likedAtKey]: {
[currentUID]: next ? serverTimestamp() : deleteField(),
},
}
[likedAtKey]: {
[currentUID]: next ? serverTimestamp() : deleteField(),
},
}
: {}),
},
};
}
await projectsRef.doc(projectId).set(
{
...(target === LIKE_TARGET.SONG
? {
// Keep legacy likedBy in sync for clients still reading this field
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
}
// Keep legacy likedBy in sync for clients still reading this field
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
}
: {}),
...likesPayload,
},
{ merge: true }
);
};
)
}
+5 -5
View File
@@ -1,7 +1,7 @@
export const normalize = (s = "") =>
export const normalize = (s = '') =>
s
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.toLowerCase()
.replace(/\s+/g, " ")
.trim();
.replace(/\s+/g, ' ')
.trim()
+148 -174
View File
@@ -1,70 +1,56 @@
import { Routes } from "../navigation/Routes";
import { Routes } from '../navigation/Routes'
export const CREATION_STAGE_KEYS = [
"songwriter",
"beatmaker",
"director",
"publisher",
];
export const CREATION_STAGE_KEYS = ['songwriter', 'beatmaker', 'director', 'publisher']
const getSectionLyrics = (section) => {
if (typeof section === "string") {
return section.trim();
if (typeof section === 'string') {
return section.trim()
}
if (section && typeof section === "object") {
const value =
typeof section.lyrics === "string" ? section.lyrics.trim() : "";
return value;
if (section && typeof section === 'object') {
const value = typeof section.lyrics === 'string' ? section.lyrics.trim() : ''
return value
}
return "";
};
return ''
}
const getLyricsCount = (project) => {
if (!project) return 0;
const { lyrics } = project;
if (!project) return 0
const { lyrics } = project
if (Array.isArray(lyrics)) {
return lyrics.reduce(
(count, section) => (getSectionLyrics(section) ? count + 1 : count),
0,
);
return lyrics.reduce((count, section) => (getSectionLyrics(section) ? count + 1 : count), 0)
}
if (lyrics && typeof lyrics === "object") {
if (lyrics && typeof lyrics === 'object') {
return Object.values(lyrics).reduce(
(count, value) => (getSectionLyrics(value) ? count + 1 : count),
0,
);
0
)
}
return getSectionLyrics(lyrics) ? 1 : 0;
};
return getSectionLyrics(lyrics) ? 1 : 0
}
const getStageMetadata = (project) => {
const lyricsCount = getLyricsCount(project);
const hasLyrics = !!(project?.hasLyrics || lyricsCount > 0);
const hasSongUrl = !!project?.songUrl;
const cover = project?.cover || {};
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)
: [];
const musicStatus = project?.musicStatus || null;
const coverStatus = project?.coverStatus || null;
const playbackStatus = project?.playbackStatus || null;
const lyricsCount = getLyricsCount(project)
const hasLyrics = !!(project?.hasLyrics || lyricsCount > 0)
const hasSongUrl = !!project?.songUrl
const cover = project?.cover || {}
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) : []
const musicStatus = project?.musicStatus || null
const coverStatus = project?.coverStatus || null
const playbackStatus = project?.playbackStatus || null
const isPlaybackGenerating =
project?.playbackGenerating === true ||
project?.isPlaybackGenerating === true ||
playbackStatus === "GENERATING";
playbackStatus === 'GENERATING'
return {
lyricsCount,
@@ -79,34 +65,34 @@ const getStageMetadata = (project) => {
playbackStatus,
youtubeStatus,
youtubeError,
isMusicGenerating: musicStatus === "GENERATING",
isCoverGenerating: coverStatus === "GENERATING",
isMusicGenerating: musicStatus === 'GENERATING',
isCoverGenerating: coverStatus === 'GENERATING',
isPlaybackGenerating,
hasYoutubePublication,
isYoutubePublishing,
};
};
}
}
const getStageLockState = (key, metadata) => {
switch (key) {
case "songwriter":
return metadata.hasSongUrl;
case "beatmaker":
case 'songwriter':
return metadata.hasSongUrl
case 'beatmaker':
if (metadata.hasCover) {
return true;
return true
}
return metadata.lyricsCount <= 0;
case "director":
return metadata.lyricsCount <= 0
case 'director':
if (metadata.hasPlaybackAsset) {
return true;
return true
}
return !metadata.hasCover;
case "publisher":
return !metadata.hasPlaybackAsset;
return !metadata.hasCover
case 'publisher':
return !metadata.hasPlaybackAsset
default:
return true;
return true
}
};
}
const getStageDescription = (key, metadata) => {
const {
@@ -117,220 +103,208 @@ const getStageDescription = (key, metadata) => {
hasMusicDraft,
isMusicGenerating,
isPlaybackGenerating,
} = metadata;
} = metadata
switch (key) {
case "songwriter":
case 'songwriter':
if (!hasLyrics) {
return "Commencer à créer des paroles pour votre chanson";
return 'Commencer à créer des paroles pour votre chanson'
}
if (!hasSongUrl) {
return "Continuer la création de paroles";
return 'Continuer la création de paroles'
}
return "Modifier les paroles créées";
case "beatmaker":
return 'Modifier les paroles créées'
case 'beatmaker':
if (metadata.hasCover) {
return "Impossible de modifier la cover ou la production musicale";
return 'Impossible de modifier la cover ou la production musicale'
}
if (!hasLyrics) {
return "Écrivez vos paroles pour débloquer la musique";
return 'Écrivez vos paroles pour débloquer la musique'
}
if (isMusicGenerating) {
return "Votre morceau est en cours de création";
return 'Votre morceau est en cours de création'
}
if (hasSongUrl) {
if (!hasCover) {
return "Créer ou modifier la cover de votre morceau";
return 'Créer ou modifier la cover de votre morceau'
}
return "Modifier la cover ou la production musicale";
return 'Modifier la cover ou la production musicale'
}
if (hasMusicDraft) {
return "Modifier la production musicale";
return 'Modifier la production musicale'
}
return "Commencer la création de votre morceau";
case "director":
return 'Commencer la création de votre morceau'
case 'director':
if (!hasCover) {
return "Générez une cover pour débloquer le playback";
return 'Générez une cover pour débloquer le playback'
}
if (isPlaybackGenerating) {
return "Votre playback est en cours de préparation";
return 'Votre playback est en cours de préparation'
}
if (hasPlaybackAsset) {
return "Modifier le playback généré";
return 'Modifier le playback généré'
}
return "Commencer la création de votre playback";
case "publisher":
return 'Commencer la création de votre playback'
case 'publisher':
if (!hasPlaybackAsset) {
return "Créez un playback pour débloquer la publication";
return 'Créez un playback pour débloquer la publication'
}
if (metadata.isYoutubePublishing) {
return "Publication de votre vidéo en cours";
return 'Publication de votre vidéo en cours'
}
if (metadata.youtubeError) {
return "La publication a échoué, réessayez.";
return 'La publication a échoué, réessayez.'
}
if (metadata.hasYoutubePublication) {
return "Votre vidéo est en ligne et prête à être partagée";
return 'Votre vidéo est en ligne et prête à être partagée'
}
return "Publier votre vidéo sur la chaîne YouTube MusicLand";
return 'Publier votre vidéo sur la chaîne YouTube MusicLand'
default:
return "";
return ''
}
};
}
const getStageLockedDescription = (key, metadata) => {
switch (key) {
case "songwriter":
return metadata.hasSongUrl
? "Impossible de modifier les paroles"
: undefined;
case "beatmaker":
case 'songwriter':
return metadata.hasSongUrl ? 'Impossible de modifier les paroles' : undefined
case 'beatmaker':
if (metadata.hasCover) {
return "Impossible de modifier la cover ou la production musicale";
return 'Impossible de modifier la cover ou la production musicale'
}
return metadata.lyricsCount > 0
? undefined
: "Créez vos paroles pour débloquer le studio";
case "director":
return metadata.hasPlaybackAsset
? "Impossible de modifier le playback"
: undefined;
case "publisher":
return metadata.lyricsCount > 0 ? undefined : 'Créez vos paroles pour débloquer le studio'
case 'director':
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 'Générez un playback pour débloquer la publication'
}
return metadata.hasYoutubePublication
? "La vidéo est déjà publiée"
: undefined;
return metadata.hasYoutubePublication ? 'La vidéo est déjà publiée' : undefined
default:
return undefined;
return undefined
}
};
}
const getStageCompletionState = (key, metadata) => {
switch (key) {
case "songwriter":
return metadata.hasLyrics;
case "beatmaker":
return metadata.hasSongUrl || metadata.musicStatus === "GENERATED";
case "director":
return metadata.hasPlaybackAsset;
case "publisher":
return metadata.hasYoutubePublication;
case 'songwriter':
return metadata.hasLyrics
case 'beatmaker':
return metadata.hasSongUrl || metadata.musicStatus === 'GENERATED'
case 'director':
return metadata.hasPlaybackAsset
case 'publisher':
return metadata.hasYoutubePublication
default:
return false;
return false
}
};
}
export const getCreationStageStates = (project) => {
const metadata = getStageMetadata(project);
const metadata = getStageMetadata(project)
return CREATION_STAGE_KEYS.map((key, index) => {
const isLocked = getStageLockState(key, metadata);
const description = getStageDescription(key, metadata);
const lockedDescription = getStageLockedDescription(key, metadata);
const isLocked = getStageLockState(key, metadata)
const description = getStageDescription(key, metadata)
const lockedDescription = getStageLockedDescription(key, metadata)
return {
key,
index,
isLocked,
isCompleted: getStageCompletionState(key, metadata),
description:
isLocked && lockedDescription ? lockedDescription : description,
description: isLocked && lockedDescription ? lockedDescription : description,
lockedDescription,
};
});
};
}
})
}
export const getStageAction = (key, project) => {
switch (key) {
case "songwriter":
case 'songwriter':
return {
route:
getLyricsCount(project) > 0 ? Routes.Lyrics : Routes.WritingLyrics,
};
case "beatmaker": {
route: getLyricsCount(project) > 0 ? Routes.Lyrics : Routes.WritingLyrics,
}
case 'beatmaker': {
if (!project) {
return { route: Routes.Compose };
return { route: Routes.Compose }
}
const musicStatus = project?.musicStatus || null;
const songUrl = project?.songUrl || null;
const coverStatus = project?.coverStatus || null;
const cover = project?.cover || {};
const hasCoverBackground = !!cover?.generatedBackground;
const hasCoverResult = !!cover?.result;
const hasFinalCover = !!project?.coverUrl;
const musicStatus = project?.musicStatus || null
const songUrl = project?.songUrl || null
const coverStatus = project?.coverStatus || null
const cover = project?.cover || {}
const hasCoverBackground = !!cover?.generatedBackground
const hasCoverResult = !!cover?.result
const hasFinalCover = !!project?.coverUrl
if (musicStatus === "GENERATING") {
return { route: Routes.GeneratingSong };
if (musicStatus === 'GENERATING') {
return { route: Routes.GeneratingSong }
}
if (!songUrl) {
if (musicStatus === "GENERATED") {
return { route: Routes.SongReady };
if (musicStatus === 'GENERATED') {
return { route: Routes.SongReady }
}
return { route: Routes.Compose };
return { route: Routes.Compose }
}
if (coverStatus === "GENERATING") {
return { route: Routes.PouchReady };
if (coverStatus === 'GENERATING') {
return { route: Routes.PouchReady }
}
if (!hasCoverBackground) {
return { route: Routes.ChooseCoverType };
return { route: Routes.ChooseCoverType }
}
if (!hasCoverResult) {
return { route: Routes.PouchReady };
return { route: Routes.PouchReady }
}
if (!hasFinalCover) {
return { route: Routes.ValidateCover };
return { route: Routes.ValidateCover }
}
return { route: Routes.ChooseCoverType };
return { route: Routes.ChooseCoverType }
}
case "director":
case 'director':
return {
route: Routes.Playback,
params: project ? { project } : undefined,
};
case "publisher": {
}
case 'publisher': {
if (!project?.id) {
return { route: Routes.PublishYoutube };
return { route: Routes.PublishYoutube }
}
return {
route: Routes.PublishYoutube,
params: { projectId: project.id },
};
}
}
default:
return {};
return {}
}
};
}
export const findFirstUnlockedStageIndex = (project) => {
const stages = getCreationStageStates(project);
const stages = getCreationStageStates(project)
if (!stages.length) {
return 0;
return 0
}
const actionableIndex = stages.findIndex(
(stage) => !stage.isCompleted && !stage.isLocked
);
const actionableIndex = stages.findIndex((stage) => !stage.isCompleted && !stage.isLocked)
if (actionableIndex !== -1) {
return actionableIndex;
return actionableIndex
}
const firstIncomplete = stages.findIndex((stage) => !stage.isCompleted);
const firstIncomplete = stages.findIndex((stage) => !stage.isCompleted)
if (firstIncomplete !== -1) {
return firstIncomplete;
return firstIncomplete
}
const firstUnlocked = stages.findIndex((stage) => !stage.isLocked);
const firstUnlocked = stages.findIndex((stage) => !stage.isLocked)
if (firstUnlocked !== -1) {
return firstUnlocked;
return firstUnlocked
}
return stages.length - 1;
};
return stages.length - 1
}
+82 -96
View File
@@ -1,187 +1,173 @@
import { SheetManager } from "react-native-actions-sheet";
import { SheetManager } from 'react-native-actions-sheet'
import appJson from "../../app.json";
import {
musiclandShareHeading,
musiclandShareMessage,
musiclandShareUrl,
} from "../data";
import appJson from '../../app.json'
import { musiclandShareHeading, musiclandShareMessage, musiclandShareUrl } from '../data'
const APP_SCHEME = appJson?.expo?.scheme || "musicland";
const APP_SCHEME_BASE = `${APP_SCHEME}://app`;
const QR_REDIRECT_PATH = "link";
const APP_SCHEME = appJson?.expo?.scheme || 'musicland'
const APP_SCHEME_BASE = `${APP_SCHEME}://app`
const QR_REDIRECT_PATH = 'link'
const trimTrailingSlash = (value) =>
typeof value === "string" ? value.replace(/\/+$/, "") : "";
const trimTrailingSlash = (value) => (typeof value === 'string' ? value.replace(/\/+$/, '') : '')
const trimLeadingSlash = (value) =>
typeof value === "string" ? value.replace(/^\/+/, "") : "";
const trimLeadingSlash = (value) => (typeof value === 'string' ? value.replace(/^\/+/, '') : '')
const buildQueryString = (params = {}) => {
const entries = Object.entries(params).filter(
([, value]) => value !== undefined && value !== null && value !== ""
);
([, value]) => value !== undefined && value !== null && value !== ''
)
if (!entries.length) {
return "";
return ''
}
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams()
entries.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item) => {
searchParams.append(key, `${item}`);
});
searchParams.append(key, `${item}`)
})
} else {
searchParams.append(key, `${value}`);
searchParams.append(key, `${value}`)
}
});
})
const query = searchParams.toString();
return query ? `?${query}` : "";
};
const query = searchParams.toString()
return query ? `?${query}` : ''
}
const createSchemeDeepLink = (path = "", params = {}) => {
const sanitizedPath = trimLeadingSlash(path);
const basePath = sanitizedPath
? `${APP_SCHEME_BASE}/${sanitizedPath}`
: APP_SCHEME_BASE;
const createSchemeDeepLink = (path = '', params = {}) => {
const sanitizedPath = trimLeadingSlash(path)
const basePath = sanitizedPath ? `${APP_SCHEME_BASE}/${sanitizedPath}` : APP_SCHEME_BASE
return `${basePath}${buildQueryString(params)}`;
};
return `${basePath}${buildQueryString(params)}`
}
const resolveBaseShareUrl = () => {
if (typeof window !== "undefined" && window.location?.origin) {
const origin = window.location.origin;
if (typeof window !== 'undefined' && window.location?.origin) {
const origin = window.location.origin
if (/localhost|127\.0\.0\.1/i.test(origin)) {
return origin;
return origin
}
}
return musiclandShareUrl;
};
return musiclandShareUrl
}
const createShareQrUrl = ({ sharePath = "", fallbackUrl } = {}) => {
const sanitizedPath = trimLeadingSlash(sharePath);
const schemeDeepLink = createSchemeDeepLink(sanitizedPath);
const baseUrl = trimTrailingSlash(resolveBaseShareUrl());
const createShareQrUrl = ({ sharePath = '', fallbackUrl } = {}) => {
const sanitizedPath = trimLeadingSlash(sharePath)
const schemeDeepLink = createSchemeDeepLink(sanitizedPath)
const baseUrl = trimTrailingSlash(resolveBaseShareUrl())
const resolvedFallback =
fallbackUrl ||
(baseUrl ? (sanitizedPath ? `${baseUrl}/${sanitizedPath}` : baseUrl) : "");
fallbackUrl || (baseUrl ? (sanitizedPath ? `${baseUrl}/${sanitizedPath}` : baseUrl) : '')
if (!baseUrl) {
return schemeDeepLink;
return schemeDeepLink
}
try {
const redirectUrl = new URL(`${baseUrl}/${QR_REDIRECT_PATH}`);
redirectUrl.searchParams.set("scheme", schemeDeepLink);
const redirectUrl = new URL(`${baseUrl}/${QR_REDIRECT_PATH}`)
redirectUrl.searchParams.set('scheme', schemeDeepLink)
if (resolvedFallback) {
redirectUrl.searchParams.set("fallback", resolvedFallback);
redirectUrl.searchParams.set('fallback', resolvedFallback)
}
return redirectUrl.toString();
return redirectUrl.toString()
} catch (error) {
console.error("share.qr.url.create.error", error);
return schemeDeepLink;
console.error('share.qr.url.create.error', error)
return schemeDeepLink
}
};
}
const defaultPayload = {
heading: musiclandShareHeading,
url: musiclandShareUrl,
shareTitle: "MusicLand",
shareTitle: 'MusicLand',
shareMessage: musiclandShareMessage,
qrUrl: createShareQrUrl({
sharePath: "",
sharePath: '',
fallbackUrl: musiclandShareUrl,
}),
copyUrl: musiclandShareUrl,
linkLabel: musiclandShareUrl,
appLink: APP_SCHEME_BASE,
};
}
export const createMusicSharePayload = ({ projectId, title, artist } = {}) => {
if (!projectId) return null;
if (!projectId) return null
const baseUrl = trimTrailingSlash(resolveBaseShareUrl());
if (!baseUrl) return null;
const encodedProjectId = encodeURIComponent(projectId);
const sharePath = `music/${encodedProjectId}`;
const shareUrl = `${baseUrl}/${sharePath}`;
const schemeDeepLink = createSchemeDeepLink(sharePath);
const baseUrl = trimTrailingSlash(resolveBaseShareUrl())
if (!baseUrl) return null
const encodedProjectId = encodeURIComponent(projectId)
const sharePath = `music/${encodedProjectId}`
const shareUrl = `${baseUrl}/${sharePath}`
const schemeDeepLink = createSchemeDeepLink(sharePath)
const qrUrl = createShareQrUrl({
sharePath,
fallbackUrl: shareUrl,
});
})
let shareMessage = musiclandShareMessage;
let shareMessage = musiclandShareMessage
if (title) {
shareMessage = `Découvre "${title}"`;
shareMessage = `Découvre "${title}"`
if (artist) {
shareMessage += ` de ${artist}`;
shareMessage += ` de ${artist}`
}
shareMessage += " sur MusicLand.";
shareMessage += ' sur MusicLand.'
}
return {
heading: "Partager le morceau",
shareTitle: title ? `${title} - MusicLand` : "MusicLand",
heading: 'Partager le morceau',
shareTitle: title ? `${title} - MusicLand` : 'MusicLand',
shareMessage,
url: shareUrl,
qrUrl,
copyUrl: shareUrl,
linkLabel: shareUrl,
appLink: schemeDeepLink,
};
};
}
}
export const createPlaybackSharePayload = ({
projectId,
title,
artist,
playbackUrl,
} = {}) => {
if (!projectId) return null;
export const createPlaybackSharePayload = ({ projectId, title, artist, playbackUrl } = {}) => {
if (!projectId) return null
const baseUrl = trimTrailingSlash(resolveBaseShareUrl());
if (!baseUrl) return null;
const encodedProjectId = encodeURIComponent(projectId);
const sharePath = `playback/${encodedProjectId}`;
const shareUrl = `${baseUrl}/${sharePath}`;
const schemeDeepLink = createSchemeDeepLink(sharePath);
const baseUrl = trimTrailingSlash(resolveBaseShareUrl())
if (!baseUrl) return null
const encodedProjectId = encodeURIComponent(projectId)
const sharePath = `playback/${encodedProjectId}`
const shareUrl = `${baseUrl}/${sharePath}`
const schemeDeepLink = createSchemeDeepLink(sharePath)
const qrUrl = createShareQrUrl({
sharePath,
fallbackUrl: playbackUrl || shareUrl,
});
})
let shareMessage = "Découvre ce playback sur MusicLand.";
let shareMessage = 'Découvre ce playback sur MusicLand.'
if (title) {
shareMessage = `Découvre "${title}"`;
shareMessage = `Découvre "${title}"`
if (artist) {
shareMessage += ` par ${artist}`;
shareMessage += ` par ${artist}`
}
shareMessage += " sur MusicLand.";
shareMessage += ' sur MusicLand.'
} else if (artist) {
shareMessage = `Découvre ce playback de ${artist} sur MusicLand.`;
shareMessage = `Découvre ce playback de ${artist} sur MusicLand.`
}
return {
heading: "Partager le playback",
shareTitle: title ? `${title} - MusicLand` : "MusicLand",
heading: 'Partager le playback',
shareTitle: title ? `${title} - MusicLand` : 'MusicLand',
shareMessage,
url: shareUrl,
qrUrl,
copyUrl: shareUrl,
linkLabel: shareUrl,
appLink: schemeDeepLink,
};
};
}
}
export const openShareSheet = (payload = {}) => {
SheetManager.show("Share", {
SheetManager.show('Share', {
payload: {
...defaultPayload,
...payload,
},
});
};
})
}
+182 -187
View File
@@ -1,84 +1,84 @@
const START_CASE_REGEX = /[_\-\s]+/g;
const START_CASE_REGEX = /[_\-\s]+/g
const toStartCase = (value) => {
const str = String(value || "")
.replace(START_CASE_REGEX, " ")
.trim();
if (!str) return "";
const str = String(value || '')
.replace(START_CASE_REGEX, ' ')
.trim()
if (!str) return ''
return str
.split(" ")
.split(' ')
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
};
.join(' ')
}
export const STRUCTURE_SEGMENTS = {
couplet: {
label: "Couplet",
promptLabel: "Couplet",
label: 'Couplet',
promptLabel: 'Couplet',
showIndex: true,
allowMultiple: true,
optional: false,
inputHeight: 225,
requiresLyrics: true,
aliases: ["vers", "verse", "couplet"],
aliases: ['vers', 'verse', 'couplet'],
},
refrain: {
label: "Refrain",
promptLabel: "Refrain",
label: 'Refrain',
promptLabel: 'Refrain',
showIndex: true,
allowMultiple: true,
optional: false,
inputHeight: 170,
requiresLyrics: true,
aliases: ["chorus", "refrain"],
aliases: ['chorus', 'refrain'],
},
short_intro: {
label: "Introduction instrumentale courte",
promptLabel: "Introduction instrumentale courte",
label: 'Introduction instrumentale courte',
promptLabel: 'Introduction instrumentale courte',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "intro",
exclusiveGroup: 'intro',
requiresLyrics: false,
aliases: [
"introduction instrumentale courte",
"intro courte",
"intro instrumentale courte",
"short intro",
'introduction instrumentale courte',
'intro courte',
'intro instrumentale courte',
'short intro',
],
},
long_intro: {
label: "Introduction instrumentale longue",
promptLabel: "Introduction instrumentale longue",
label: 'Introduction instrumentale longue',
promptLabel: 'Introduction instrumentale longue',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "intro",
exclusiveGroup: 'intro',
requiresLyrics: false,
aliases: [
"introduction instrumentale longue",
"intro longue",
"long intro",
"intro instrumentale longue",
'introduction instrumentale longue',
'intro longue',
'long intro',
'intro instrumentale longue',
],
},
pre_refrain_instrumental: {
label: "Pré-refrain instrumental",
promptLabel: "Pré-refrain instrumental",
label: 'Pré-refrain instrumental',
promptLabel: 'Pré-refrain instrumental',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "pre_chorus",
exclusiveGroup: 'pre_chorus',
requiresLyrics: false,
aliases: [],
},
pont: {
label: "Pont",
promptLabel: "Pont",
label: 'Pont',
promptLabel: 'Pont',
showIndex: false,
allowMultiple: true,
optional: true,
@@ -87,8 +87,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
solo_de_guitare: {
label: "Solo de guitare",
promptLabel: "Solo de guitare",
label: 'Solo de guitare',
promptLabel: 'Solo de guitare',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -97,8 +97,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
solo_de_guitare_electrique: {
label: "Solo de guitare électrique",
promptLabel: "Solo de guitare électrique",
label: 'Solo de guitare électrique',
promptLabel: 'Solo de guitare électrique',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -107,8 +107,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
solo_de_batterie: {
label: "Solo de batterie",
promptLabel: "Solo de batterie",
label: 'Solo de batterie',
promptLabel: 'Solo de batterie',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -117,8 +117,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
solo_de_saxophone: {
label: "Solo de saxophone",
promptLabel: "Solo de saxophone",
label: 'Solo de saxophone',
promptLabel: 'Solo de saxophone',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -127,8 +127,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
solo_de_violon: {
label: "Solo de violon",
promptLabel: "Solo de violon",
label: 'Solo de violon',
promptLabel: 'Solo de violon',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -137,8 +137,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
break: {
label: "Break",
promptLabel: "Break",
label: 'Break',
promptLabel: 'Break',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -147,8 +147,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
interlude: {
label: "Interlude",
promptLabel: "Interlude",
label: 'Interlude',
promptLabel: 'Interlude',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -157,8 +157,8 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
interlude_melodique: {
label: "Interlude mélodique",
promptLabel: "Interlude mélodique",
label: 'Interlude mélodique',
promptLabel: 'Interlude mélodique',
showIndex: true,
allowMultiple: true,
optional: true,
@@ -167,240 +167,235 @@ export const STRUCTURE_SEGMENTS = {
aliases: [],
},
final_apogee: {
label: "Final apogée",
promptLabel: "Final apogée",
label: 'Final apogée',
promptLabel: 'Final apogée',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 170,
exclusiveGroup: "outro",
exclusiveGroup: 'outro',
requiresLyrics: false,
aliases: [],
},
arret_net: {
label: "Arrêt net",
promptLabel: "Arrêt net",
label: 'Arrêt net',
promptLabel: 'Arrêt net',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "outro",
exclusiveGroup: 'outro',
requiresLyrics: false,
aliases: [],
},
fade_out: {
label: "Fade out",
promptLabel: "Fade out",
label: 'Fade out',
promptLabel: 'Fade out',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "outro",
exclusiveGroup: 'outro',
requiresLyrics: false,
aliases: [],
},
transition_douce: {
label: "Transition douce vers le silence",
promptLabel: "Transition douce",
label: 'Transition douce vers le silence',
promptLabel: 'Transition douce',
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "outro",
exclusiveGroup: 'outro',
requiresLyrics: false,
aliases: [],
},
};
}
const ALIAS_TO_KEY = Object.entries(STRUCTURE_SEGMENTS).reduce(
(acc, [key, meta]) => {
const aliases = Array.isArray(meta.aliases) ? meta.aliases : [];
aliases.forEach((alias) => {
acc[String(alias).toLowerCase()] = key;
});
return acc;
},
{}
);
const ALIAS_TO_KEY = Object.entries(STRUCTURE_SEGMENTS).reduce((acc, [key, meta]) => {
const aliases = Array.isArray(meta.aliases) ? meta.aliases : []
aliases.forEach((alias) => {
acc[String(alias).toLowerCase()] = key
})
return acc
}, {})
const LEGACY_STRUCTURE_ALIASES = {
pre_chorus: "pre_refrain_instrumental",
"pré-refrain": "pre_refrain_instrumental",
"pre-refrain": "pre_refrain_instrumental",
"pre_refrain": "pre_refrain_instrumental",
prechorus: "pre_refrain_instrumental",
"pre chorus": "pre_refrain_instrumental",
"pre-chorus": "pre_refrain_instrumental",
instrumental_pre_chorus: "pre_refrain_instrumental",
bridge: "pont",
guitar_solo: "solo_de_guitare",
electric_guitar_solo: "solo_de_guitare_electrique",
drum_solo: "solo_de_batterie",
sax_solo: "solo_de_saxophone",
violin_solo: "solo_de_violon",
melodic_interlude: "interlude_melodique",
grand_finale: "final_apogee",
sudden_stop: "arret_net",
soft_transition: "transition_douce",
};
pre_chorus: 'pre_refrain_instrumental',
'pré-refrain': 'pre_refrain_instrumental',
'pre-refrain': 'pre_refrain_instrumental',
pre_refrain: 'pre_refrain_instrumental',
prechorus: 'pre_refrain_instrumental',
'pre chorus': 'pre_refrain_instrumental',
'pre-chorus': 'pre_refrain_instrumental',
instrumental_pre_chorus: 'pre_refrain_instrumental',
bridge: 'pont',
guitar_solo: 'solo_de_guitare',
electric_guitar_solo: 'solo_de_guitare_electrique',
drum_solo: 'solo_de_batterie',
sax_solo: 'solo_de_saxophone',
violin_solo: 'solo_de_violon',
melodic_interlude: 'interlude_melodique',
grand_finale: 'final_apogee',
sudden_stop: 'arret_net',
soft_transition: 'transition_douce',
}
Object.entries(LEGACY_STRUCTURE_ALIASES).forEach(([alias, target]) => {
const key = String(alias).toLowerCase();
ALIAS_TO_KEY[key] = target;
});
const key = String(alias).toLowerCase()
ALIAS_TO_KEY[key] = target
})
const sanitizeToken = (value) =>
String(value || "")
String(value || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
export const getSegmentMeta = (type) => {
const key = String(type || "").toLowerCase();
return STRUCTURE_SEGMENTS[key] || null;
};
const key = String(type || '').toLowerCase()
return STRUCTURE_SEGMENTS[key] || null
}
export const segmentRequiresLyrics = (type) => {
const meta = getSegmentMeta(type);
if (!meta) return true;
return meta.requiresLyrics !== false;
};
const meta = getSegmentMeta(type)
if (!meta) return true
return meta.requiresLyrics !== false
}
export const OPTIONAL_STRUCTURE_SEGMENTS = Object.keys(
STRUCTURE_SEGMENTS
).filter((key) => STRUCTURE_SEGMENTS[key]?.optional);
export const OPTIONAL_STRUCTURE_SEGMENTS = Object.keys(STRUCTURE_SEGMENTS).filter(
(key) => STRUCTURE_SEGMENTS[key]?.optional
)
export const normalizeStructureType = (value) => {
const raw = String(value || "").trim();
if (!raw) return "";
const lower = raw.toLowerCase();
if (STRUCTURE_SEGMENTS[lower]) return lower;
if (ALIAS_TO_KEY[lower]) return ALIAS_TO_KEY[lower];
const raw = String(value || '').trim()
if (!raw) return ''
const lower = raw.toLowerCase()
if (STRUCTURE_SEGMENTS[lower]) return lower
if (ALIAS_TO_KEY[lower]) return ALIAS_TO_KEY[lower]
const sanitized = sanitizeToken(raw);
if (STRUCTURE_SEGMENTS[sanitized]) return sanitized;
if (ALIAS_TO_KEY[sanitized]) return ALIAS_TO_KEY[sanitized];
return sanitized;
};
const sanitized = sanitizeToken(raw)
if (STRUCTURE_SEGMENTS[sanitized]) return sanitized
if (ALIAS_TO_KEY[sanitized]) return ALIAS_TO_KEY[sanitized]
return sanitized
}
export const sanitizeStructureList = (structure = [], options = {}) => {
if (!Array.isArray(structure)) return [];
const { autoInjectPreChorus = true } = options || {};
const output = [];
let shouldInjectInstrumentalPreChorus = false;
if (!Array.isArray(structure)) return []
const { autoInjectPreChorus = true } = options || {}
const output = []
let shouldInjectInstrumentalPreChorus = false
structure.forEach((segment) => {
const type = normalizeStructureType(segment);
if (!type) return;
const meta = getSegmentMeta(type);
const type = normalizeStructureType(segment)
if (!type) return
const meta = getSegmentMeta(type)
if (meta) {
if (meta.allowMultiple === false) {
const alreadyIncluded = output.some((item) => item === type);
if (alreadyIncluded) return;
const alreadyIncluded = output.some((item) => item === type)
if (alreadyIncluded) return
}
if (meta.exclusiveGroup) {
const previousIndex = output.findIndex((item) => {
const existingMeta = getSegmentMeta(item);
return existingMeta?.exclusiveGroup === meta.exclusiveGroup;
});
const existingMeta = getSegmentMeta(item)
return existingMeta?.exclusiveGroup === meta.exclusiveGroup
})
if (previousIndex !== -1) {
output.splice(previousIndex, 1);
output.splice(previousIndex, 1)
}
}
}
if (type === "pre_refrain_instrumental" && autoInjectPreChorus) {
shouldInjectInstrumentalPreChorus = true;
return;
if (type === 'pre_refrain_instrumental' && autoInjectPreChorus) {
shouldInjectInstrumentalPreChorus = true
return
}
output.push(type);
});
output.push(type)
})
let result = output;
let result = output
if (autoInjectPreChorus && shouldInjectInstrumentalPreChorus) {
const expanded = [];
let hasRefrain = false;
const expanded = []
let hasRefrain = false
result.forEach((item) => {
if (item === "refrain") {
hasRefrain = true;
const previous = expanded[expanded.length - 1];
if (previous !== "pre_refrain_instrumental") {
expanded.push("pre_refrain_instrumental");
if (item === 'refrain') {
hasRefrain = true
const previous = expanded[expanded.length - 1]
if (previous !== 'pre_refrain_instrumental') {
expanded.push('pre_refrain_instrumental')
}
}
expanded.push(item);
});
expanded.push(item)
})
if (!hasRefrain) {
expanded.push("pre_refrain_instrumental");
expanded.push('pre_refrain_instrumental')
}
result = expanded;
result = expanded
}
const introIndex = result.findIndex(
(item) => item === "short_intro" || item === "long_intro"
);
const introIndex = result.findIndex((item) => item === 'short_intro' || item === 'long_intro')
if (introIndex > 0) {
const [intro] = result.splice(introIndex, 1);
result.unshift(intro);
const [intro] = result.splice(introIndex, 1)
result.unshift(intro)
}
const outroIndex = result.findIndex((item) => {
const meta = getSegmentMeta(item);
return meta?.exclusiveGroup === "outro";
});
const meta = getSegmentMeta(item)
return meta?.exclusiveGroup === 'outro'
})
if (outroIndex !== -1 && outroIndex < result.length - 1) {
const [outro] = result.splice(outroIndex, 1);
result.push(outro);
const [outro] = result.splice(outroIndex, 1)
result.push(outro)
}
return result;
};
return result
}
export const formatStructureLabel = (type, occurrence = 1) => {
const normalized = normalizeStructureType(type);
const meta = getSegmentMeta(normalized);
const baseLabel = meta?.label || toStartCase(normalized || type);
const showIndex = meta?.showIndex === true;
if (showIndex) return `${baseLabel} ${occurrence}`;
if (!meta && occurrence > 1) return `${baseLabel} ${occurrence}`;
return baseLabel;
};
const normalized = normalizeStructureType(type)
const meta = getSegmentMeta(normalized)
const baseLabel = meta?.label || toStartCase(normalized || type)
const showIndex = meta?.showIndex === true
if (showIndex) return `${baseLabel} ${occurrence}`
if (!meta && occurrence > 1) return `${baseLabel} ${occurrence}`
return baseLabel
}
export const getStructureInputHeight = (type) => {
const normalized = normalizeStructureType(type);
const meta = getSegmentMeta(normalized);
return typeof meta?.inputHeight === "number" ? meta.inputHeight : undefined;
};
const normalized = normalizeStructureType(type)
const meta = getSegmentMeta(normalized)
return typeof meta?.inputHeight === 'number' ? meta.inputHeight : undefined
}
export const getPromptLabelForStructure = (type) => {
const normalized = normalizeStructureType(type);
const meta = getSegmentMeta(normalized);
if (meta?.promptLabel) return meta.promptLabel;
if (meta?.label) return meta.label;
return toStartCase(normalized || type);
};
const normalized = normalizeStructureType(type)
const meta = getSegmentMeta(normalized)
if (meta?.promptLabel) return meta.promptLabel
if (meta?.label) return meta.label
return toStartCase(normalized || type)
}
export const isOptionalStructureType = (type) => {
const normalized = normalizeStructureType(type);
const meta = getSegmentMeta(normalized);
return !!meta?.optional;
};
const normalized = normalizeStructureType(type)
const meta = getSegmentMeta(normalized)
return !!meta?.optional
}
export const arraysAreSame = (a = [], b = []) => {
if (a === b) return true;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
if (a === b) return true
if (!Array.isArray(a) || !Array.isArray(b)) return false
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
if (a[i] !== b[i]) return false
}
return true;
};
return true
}