feat: tickets

This commit is contained in:
2025-12-08 14:13:39 +01:00
parent 8c6cc50dcb
commit b0cf4a20ab
3 changed files with 290 additions and 273 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ const GlobalAudioPlayer = () => {
} }
const project = const project =
currentTrack?.metadata?.project && currentTrack?.metadata?.project &&
typeof currentTrack.metadata.project === "object" typeof currentTrack.metadata.project === "object"
? currentTrack.metadata.project ? currentTrack.metadata.project
: null; : null;
navigateToMusicDetails({ navigateToMusicDetails({
+1
View File
@@ -42,6 +42,7 @@ const HIDDEN_ROUTE_NAMES = new Set([
Routes.PlaybackExample, Routes.PlaybackExample,
Routes.PlaybackOnboarding, Routes.PlaybackOnboarding,
Routes.Playback, Routes.Playback,
Routes.PlaybackGuide,
Routes.RecordPlayback, Routes.RecordPlayback,
Routes.RecordedPlayback, Routes.RecordedPlayback,
Routes.ChooseDecor, Routes.ChooseDecor,
+288 -272
View File
@@ -1,6 +1,12 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Image as ExpoImage } from "expo-image"; import { Image as ExpoImage } from "expo-image";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Image, Image,
@@ -9,6 +15,8 @@ import {
Text, Text,
TextInput, TextInput,
View, View,
KeyboardAvoidingView,
Platform,
} from "react-native"; } from "react-native";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
import alert from "../../components/Alert"; import alert from "../../components/Alert";
@@ -220,10 +228,7 @@ const PouchReady = () => {
const start = coverProgressStartRef.current; const start = coverProgressStartRef.current;
if (!start) return; if (!start) return;
const elapsed = Date.now() - start.getTime(); const elapsed = Date.now() - start.getTime();
const ratio = Math.max( const ratio = Math.max(0, Math.min(1, elapsed / COVER_FAKE_DURATION_MS));
0,
Math.min(1, elapsed / COVER_FAKE_DURATION_MS),
);
const next = COVER_PROGRESS_MAX * ratio; const next = COVER_PROGRESS_MAX * ratio;
setCoverProgress((prev) => { setCoverProgress((prev) => {
if (prev >= COVER_PROGRESS_MAX) return COVER_PROGRESS_MAX; if (prev >= COVER_PROGRESS_MAX) return COVER_PROGRESS_MAX;
@@ -310,45 +315,82 @@ const PouchReady = () => {
return ( return (
<Page backgroundImg={background.studioBG2} headerType="NONE"> <Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader onPressBack={goBack} progress={72} /> <MusicLandHeader onPressBack={goBack} progress={72} />
<View style={{ flex: 1, marginTop: 0 }}> <KeyboardAvoidingView
<CreateLyricsHeader behavior={Platform.OS === "ios" ? "padding" : "height"}
title={ style={{ flex: 1 }}
selectedProject?.cover?.backgroundGenerated >
? "Ta pochette est prête!" <View style={{ flex: 1, marginTop: 0 }}>
: "Génération de la pochette" <CreateLyricsHeader
} title={
/> selectedProject?.cover?.backgroundGenerated
<View style={{ flex: 1, ...Style.containerCenter }}> ? "Ta pochette est prête!"
<View style={{ width: isWeb ? "80%" : "100%", gap: 24 }}> : "Génération de la pochette"
<View }
style={{ />
width: "100%", <View style={{ flex: 1, ...Style.containerCenter }}>
flexDirection: isWeb ? "row" : "column", <View style={{ width: isWeb ? "80%" : "80%", gap: 24 }}>
gap: 16, <View
justifyContent: "center", style={{
flexWrap: "wrap", width: "100%",
}} flexDirection: isWeb ? "row" : "column",
> gap: 16,
{displayOptions.length > 0 ? ( justifyContent: "center",
displayOptions.map((option, index) => { flexWrap: "wrap",
const optionUri = }}
option?.finalUrl || option?.generatedUrl || ""; >
if (!optionUri) return null; {displayOptions.length > 0 ? (
if (hasGeneratedOptions) { displayOptions.map((option, index) => {
const isSelected = option?.id === selectedOption?.id; const optionUri =
const cardWidthStyle = isWeb option?.finalUrl || option?.generatedUrl || "";
? styles.coverOptionCardWeb if (!optionUri) return null;
: styles.coverOptionCardMobile; if (hasGeneratedOptions) {
const isSelected = option?.id === selectedOption?.id;
const cardWidthStyle = isWeb
? styles.coverOptionCardWeb
: styles.coverOptionCardMobile;
return (
<Pressable
key={option?.id || index}
onPress={() => handleSelectOption(option)}
disabled={isSelecting}
style={[
styles.coverOptionCard,
cardWidthStyle,
isSelected ? styles.coverOptionCardSelected : null,
isSelecting ? styles.coverOptionCardDisabled : null,
]}
>
<ExpoImage
source={{ uri: optionUri }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={120}
style={styles.coverOptionImage}
/>
<View style={styles.coverOptionBadge}>
<Text style={styles.coverOptionBadgeLabel}>
{`Option ${index + 1}`}
</Text>
</View>
{isSelected && (
<View style={styles.coverOptionSelectedBadge}>
<Text style={styles.coverOptionSelectedLabel}>
Sélectionnée
</Text>
</View>
)}
</Pressable>
);
}
return ( return (
<Pressable <View
key={option?.id || index} key={option?.id || index}
onPress={() => handleSelectOption(option)}
disabled={isSelecting}
style={[ style={[
styles.coverOptionCard, styles.coverPreviewCard,
cardWidthStyle, isWeb
isSelected ? styles.coverOptionCardSelected : null, ? styles.coverPreviewCardWeb
isSelecting ? styles.coverOptionCardDisabled : null, : styles.coverPreviewCardMobile,
]} ]}
> >
<ExpoImage <ExpoImage
@@ -357,62 +399,41 @@ const PouchReady = () => {
priority="high" priority="high"
contentFit="cover" contentFit="cover"
transition={120} transition={120}
style={styles.coverOptionImage} style={styles.coverPreviewImage}
/> />
<View style={styles.coverOptionBadge}> </View>
<Text style={styles.coverOptionBadgeLabel}>
{`Option ${index + 1}`}
</Text>
</View>
{isSelected && (
<View style={styles.coverOptionSelectedBadge}>
<Text style={styles.coverOptionSelectedLabel}>
Sélectionnée
</Text>
</View>
)}
</Pressable>
); );
} })
return ( ) : (
<View <>
key={option?.id || index} {isGenerating && (
style={[ <View
styles.coverPreviewCard, style={{
isWeb alignItems: "center",
? styles.coverPreviewCardWeb gap: 8,
: styles.coverPreviewCardMobile, width: isWeb ? 300 : "100%",
]} height: 300,
> borderRadius: 20,
<ExpoImage backgroundColor: "#00000040",
source={{ uri: optionUri }} alignSelf: "center",
cachePolicy="memory-disk" ...Style.containerCenter,
priority="high" }}
contentFit="cover" >
transition={120} <ActivityIndicator color={Palette.white} />
style={styles.coverPreviewImage} {isWeb ? (
/> coverBackgroundMessage ? (
</View> <Text
); style={{
}) color: Palette.white,
) : ( marginTop: 6,
<> textAlign: "center",
{isGenerating && ( opacity: 0.9,
<View }}
style={{ >
alignItems: "center", {coverBackgroundMessage}
gap: 8, </Text>
width: isWeb ? 300 : "100%", ) : null
height: 300, ) : (
borderRadius: 20,
backgroundColor: "#00000040",
alignSelf: "center",
...Style.containerCenter,
}}
>
<ActivityIndicator color={Palette.white} />
{isWeb ? (
coverBackgroundMessage ? (
<Text <Text
style={{ style={{
color: Palette.white, color: Palette.white,
@@ -421,201 +442,196 @@ const PouchReady = () => {
opacity: 0.9, opacity: 0.9,
}} }}
> >
{coverBackgroundMessage} Génération en cours.
</Text>
)}
<View style={{ alignItems: "center", gap: 10 }}>
<ProgressBar gradient progress={coverProgressValue} />
<Text
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
}}
>
{coverProgressValue}%
</Text> </Text>
) : null
) : (
<Text
style={{
color: Palette.white,
marginTop: 6,
textAlign: "center",
opacity: 0.9,
}}
>
Génération en cours.
</Text>
)}
<View style={{ alignItems: "center", gap: 10 }}>
<ProgressBar gradient progress={coverProgressValue} />
<Text
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
}}
>
{coverProgressValue}%
</Text>
</View>
</View>
)}
</>
// </View>
)}
</View>
{shouldShowStylePanel && (
<BlurView
tint="dark"
intensity={isWeb ? 40 : 30}
style={[styles.stylePanel, isWeb && styles.stylePanelWeb]}
>
<View style={styles.stylePanelHeader}>
<Text style={styles.panelTitle}>Style de la pochette</Text>
<Text style={styles.panelSubtitle}>
Choisis une ambiance ou écris ton propre brief créatif.
</Text>
</View>
<View style={styles.modeContainer}>
<View
style={[
styles.modeCard,
styleMode === "preset" ? styles.modeCardActive : null,
]}
>
<AppCheckbox
label="Choisir un style prédéfini"
selected={styleMode === "preset"}
onPress={() => {
setStyleMode("preset");
setIsPresetDropdownOpen(false);
if (!selectedPresetStyle) {
setSelectedPresetStyle(COVER_STYLE_PRESETS[0]);
}
}}
/>
</View>
{styleMode === "preset" && (
<View style={styles.dropdownArea}>
<Pressable
style={styles.dropdownTrigger}
onPress={() => setIsPresetDropdownOpen((prev) => !prev)}
>
<Text
style={styles.dropdownTriggerLabel}
numberOfLines={2}
>
{selectedPresetStyle}
</Text>
<Image
source={icons.chevronDown}
style={[
styles.dropdownArrow,
isPresetDropdownOpen
? styles.dropdownArrowOpen
: null,
]}
/>
</Pressable>
{isPresetDropdownOpen && (
<View style={styles.dropdownList}>
{COVER_STYLE_PRESETS.map((styleOption, index) => {
const isActive =
selectedPresetStyle === styleOption;
const isLast =
index === COVER_STYLE_PRESETS.length - 1;
return (
<Pressable
key={styleOption}
style={[
styles.dropdownOption,
!isLast ? styles.dropdownOptionDivider : null,
isActive ? styles.dropdownOptionActive : null,
]}
onPress={() => {
setSelectedPresetStyle(styleOption);
setIsPresetDropdownOpen(false);
}}
>
<Text
style={[
styles.dropdownOptionLabel,
isActive
? styles.dropdownOptionLabelActive
: null,
]}
>
{styleOption}
</Text>
{isActive && (
<Image
source={icons.check}
style={styles.dropdownOptionIcon}
resizeMode="contain"
/>
)}
</Pressable>
);
})}
</View> </View>
)} </View>
</View> )}
)} </>
<View style={styles.modeColumn}> // </View>
)}
</View>
{shouldShowStylePanel && (
<BlurView
tint="dark"
intensity={isWeb ? 40 : 30}
style={[styles.stylePanel, isWeb && styles.stylePanelWeb]}
>
<View style={styles.stylePanelHeader}>
<Text style={styles.panelTitle}>Style de la pochette</Text>
<Text style={styles.panelSubtitle}>
Choisis une ambiance ou écris ton propre brief créatif.
</Text>
</View>
<View style={styles.modeContainer}>
<View <View
style={[ style={[
styles.modeCard, styles.modeCard,
styleMode === "custom" ? styles.modeCardActive : null, styleMode === "preset" ? styles.modeCardActive : null,
]} ]}
> >
<AppCheckbox <AppCheckbox
label="Style personnalisé" label="Choisir un style prédéfini"
selected={styleMode === "custom"} selected={styleMode === "preset"}
onPress={() => { onPress={() => {
setStyleMode("custom"); setStyleMode("preset");
setIsPresetDropdownOpen(false); setIsPresetDropdownOpen(false);
if (!selectedPresetStyle) {
setSelectedPresetStyle(COVER_STYLE_PRESETS[0]);
}
}} }}
/> />
</View> </View>
{styleMode === "custom" && ( {styleMode === "preset" && (
<View style={styles.customInputWrapper}> <View style={styles.dropdownArea}>
<TextInput <Pressable
placeholder="Exemple : Collage rétro futuriste lumineux" style={styles.dropdownTrigger}
placeholderTextColor={Palette.grayMid} onPress={() =>
value={customStyle} setIsPresetDropdownOpen((prev) => !prev)
onChangeText={setCustomStyle} }
style={styles.customInput} >
multiline={false} <Text
maxLength={120} style={styles.dropdownTriggerLabel}
textAlignVertical="center" numberOfLines={2}
autoCapitalize="sentences" >
autoCorrect {selectedPresetStyle}
selectionColor={Palette.primary} </Text>
/> <Image
source={icons.chevronDown}
style={[
styles.dropdownArrow,
isPresetDropdownOpen
? styles.dropdownArrowOpen
: null,
]}
/>
</Pressable>
{isPresetDropdownOpen && (
<View style={styles.dropdownList}>
{COVER_STYLE_PRESETS.map((styleOption, index) => {
const isActive =
selectedPresetStyle === styleOption;
const isLast =
index === COVER_STYLE_PRESETS.length - 1;
return (
<Pressable
key={styleOption}
style={[
styles.dropdownOption,
!isLast
? styles.dropdownOptionDivider
: null,
isActive
? styles.dropdownOptionActive
: null,
]}
onPress={() => {
setSelectedPresetStyle(styleOption);
setIsPresetDropdownOpen(false);
}}
>
<Text
style={[
styles.dropdownOptionLabel,
isActive
? styles.dropdownOptionLabelActive
: null,
]}
>
{styleOption}
</Text>
{isActive && (
<Image
source={icons.check}
style={styles.dropdownOptionIcon}
resizeMode="contain"
/>
)}
</Pressable>
);
})}
</View>
)}
</View> </View>
)} )}
<View style={styles.modeColumn}>
<View
style={[
styles.modeCard,
styleMode === "custom" ? styles.modeCardActive : null,
]}
>
<AppCheckbox
label="Style personnalisé"
selected={styleMode === "custom"}
onPress={() => {
setStyleMode("custom");
setIsPresetDropdownOpen(false);
}}
/>
</View>
{styleMode === "custom" && (
<View style={styles.customInputWrapper}>
<TextInput
placeholder="Exemple : Collage rétro futuriste lumineux"
placeholderTextColor={Palette.grayMid}
value={customStyle}
onChangeText={setCustomStyle}
style={styles.customInput}
multiline={false}
maxLength={120}
textAlignVertical="center"
autoCapitalize="sentences"
autoCorrect
selectionColor={Palette.primary}
/>
</View>
)}
</View>
</View> </View>
</View> </BlurView>
</BlurView> )}
)} </View>
</View> </View>
</View> </View>
</View> <View
<View style={{
style={{ paddingBottom: gutters * 2,
paddingBottom: gutters * 2, width: isWeb ? "80%" : "100%",
width: isWeb ? "80%" : "100%", alignSelf: "center",
alignSelf: "center", gap: 12,
gap: 12, }}
}} >
> {!hasGeneratedOptions && (
{!hasGeneratedOptions && ( <BorderGradientButton
<BorderGradientButton title={
title={ isGenerating ? "Génération en cours..." : "Générer la pochette"
isGenerating ? "Génération en cours..." : "Générer la pochette" }
} icon={icons.stars}
icon={icons.stars} onPress={requestCoverGeneration}
onPress={requestCoverGeneration} disabled={isGenerating}
disabled={isGenerating} />
/> )}
)} {hasGeneratedOptions && (
{hasGeneratedOptions && ( <GradientButton
<GradientButton title={"Valider la pochette"}
title={"Valider la pochette"} onPress={onValidatePicture}
onPress={onValidatePicture} />
/> )}
)} </View>
</View> </KeyboardAvoidingView>
</Page> </Page>
); );
}; };