@umituz/react-native-ai-generation-content 1.84.3 → 1.84.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/domain/entities/flow-step.types.ts +1 -0
- package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +4 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +1 -0
- package/src/domains/generation/wizard/configs/index.ts +1 -0
- package/src/domains/generation/wizard/configs/solo-video.config.ts +45 -0
- package/src/domains/generation/wizard/domain/entities/wizard-step.types.ts +12 -0
- package/src/domains/generation/wizard/index.ts +5 -1
- package/src/domains/generation/wizard/infrastructure/builders/dynamic-step-builder.ts +1 -0
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +4 -0
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.utils.ts +7 -0
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderAudioPickerStep.tsx +56 -0
- package/src/domains/generation/wizard/presentation/screens/AudioPickerScreen.tsx +222 -0
- package/src/domains/generation/wizard/presentation/screens/AudioPickerScreen.types.ts +30 -0
- package/src/domains/generation/wizard/presentation/screens/index.ts +5 -0
- package/src/domains/result-preview/presentation/components/ResultActionBar.tsx +10 -0
- package/src/domains/result-preview/presentation/components/ResultPreviewScreen.tsx +2 -0
- package/src/domains/result-preview/presentation/types/result-components.types.ts +2 -0
- package/src/domains/result-preview/presentation/types/result-screen.types.ts +2 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.84.
|
|
3
|
+
"version": "1.84.5",
|
|
4
4
|
"description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"expo-clipboard": "^8.0.8",
|
|
85
85
|
"expo-crypto": "^15.0.8",
|
|
86
86
|
"expo-device": "^8.0.10",
|
|
87
|
+
"expo-document-picker": "^14.0.8",
|
|
87
88
|
"expo-file-system": "^19.0.21",
|
|
88
89
|
"expo-font": "^14.0.10",
|
|
89
90
|
"expo-haptics": "^15.0.8",
|
|
@@ -27,6 +27,8 @@ interface GalleryResultPreviewProps {
|
|
|
27
27
|
readonly onEdit?: (imageUrl: string) => void;
|
|
28
28
|
/** Called when the user taps Edit on a video creation. */
|
|
29
29
|
readonly onEditVideo?: (videoUrl: string) => void;
|
|
30
|
+
/** Called when the user taps Post to Feed. Omit to hide (apps without a feed). */
|
|
31
|
+
readonly onShareToFeed?: (creation: Creation) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function GalleryResultPreview({
|
|
@@ -43,6 +45,7 @@ export function GalleryResultPreview({
|
|
|
43
45
|
onCloseRating,
|
|
44
46
|
onEdit,
|
|
45
47
|
onEditVideo,
|
|
48
|
+
onShareToFeed,
|
|
46
49
|
}: GalleryResultPreviewProps) {
|
|
47
50
|
const alert = useAlert();
|
|
48
51
|
|
|
@@ -69,6 +72,7 @@ export function GalleryResultPreview({
|
|
|
69
72
|
onRate={onRate}
|
|
70
73
|
onEdit={!videoUrl && imageUrl && onEdit ? () => onEdit(imageUrl) : undefined}
|
|
71
74
|
onEditVideo={videoUrl && onEditVideo ? () => onEditVideo(videoUrl) : undefined}
|
|
75
|
+
onShareToFeed={onShareToFeed ? () => onShareToFeed(selectedCreation) : undefined}
|
|
72
76
|
hideLabel
|
|
73
77
|
iconOnly
|
|
74
78
|
showTryAgain
|
|
@@ -6,3 +6,4 @@
|
|
|
6
6
|
export { TEXT_TO_IMAGE_WIZARD_CONFIG } from "./text-to-image.config";
|
|
7
7
|
export { TEXT_TO_VIDEO_WIZARD_CONFIG } from "./text-to-video.config";
|
|
8
8
|
export { IMAGE_TO_VIDEO_WIZARD_CONFIG } from "./image-to-video.config";
|
|
9
|
+
export { SOLO_VIDEO_WIZARD_CONFIG } from "./solo-video.config";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solo Video Wizard Config
|
|
3
|
+
* Flow: Photo → Prompt (with info about two-step generation) → Audio (optional) → Generation
|
|
4
|
+
*
|
|
5
|
+
* Two-step generation:
|
|
6
|
+
* 1. Generate image from photo + prompt (nano-banana-2/edit)
|
|
7
|
+
* 2. Generate video from that image (I2V model)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { WizardFeatureConfig } from "../domain/entities/wizard-feature.types";
|
|
11
|
+
|
|
12
|
+
export const SOLO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
|
|
13
|
+
id: "solo-video",
|
|
14
|
+
name: "Solo Video",
|
|
15
|
+
steps: [
|
|
16
|
+
{
|
|
17
|
+
id: "photo_1",
|
|
18
|
+
type: "photo_upload",
|
|
19
|
+
titleKey: "soloVideo.selectPhoto",
|
|
20
|
+
subtitleKey: "soloVideo.selectPhotoHint",
|
|
21
|
+
showFaceDetection: false,
|
|
22
|
+
showPhotoTips: true,
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "video_prompt",
|
|
27
|
+
type: "text_input",
|
|
28
|
+
titleKey: "soloVideo.prompt",
|
|
29
|
+
subtitleKey: "soloVideo.promptInfo",
|
|
30
|
+
placeholderKey: "soloVideo.promptPlaceholder",
|
|
31
|
+
required: true,
|
|
32
|
+
minLength: 3,
|
|
33
|
+
maxLength: 500,
|
|
34
|
+
multiline: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "background_audio",
|
|
38
|
+
type: "audio_picker",
|
|
39
|
+
titleKey: "soloVideo.audioTitle",
|
|
40
|
+
subtitleKey: "soloVideo.audioSubtitle",
|
|
41
|
+
required: false,
|
|
42
|
+
maxFileSizeMB: 20,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
@@ -84,6 +84,17 @@ export interface CreditGateStepConfig extends BaseStepConfig {
|
|
|
84
84
|
readonly messageKey?: string;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Audio Picker Step Configuration
|
|
89
|
+
*/
|
|
90
|
+
export interface AudioPickerStepConfig extends BaseStepConfig {
|
|
91
|
+
readonly type: "audio_picker";
|
|
92
|
+
/** Allowed MIME types (default: audio/mpeg, audio/mp4, audio/wav, audio/aac) */
|
|
93
|
+
readonly allowedTypes?: readonly string[];
|
|
94
|
+
/** Max file size in MB (default: 20) */
|
|
95
|
+
readonly maxFileSizeMB?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
/**
|
|
88
99
|
* Union of all step config types
|
|
89
100
|
*/
|
|
@@ -94,4 +105,5 @@ export type WizardStepConfig =
|
|
|
94
105
|
| TextInputStepConfig
|
|
95
106
|
| SelectionStepConfig
|
|
96
107
|
| PreviewStepConfig
|
|
108
|
+
| AudioPickerStepConfig
|
|
97
109
|
| BaseStepConfig;
|
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
TextInputStepConfig,
|
|
14
14
|
SelectionStepConfig,
|
|
15
15
|
PreviewStepConfig,
|
|
16
|
+
AudioPickerStepConfig,
|
|
16
17
|
WizardStepConfig,
|
|
17
18
|
} from "./domain/entities/wizard-step.types";
|
|
18
19
|
|
|
@@ -70,11 +71,13 @@ export { GenericWizardFlow } from "./presentation/components";
|
|
|
70
71
|
export type { GenericWizardFlowProps } from "./presentation/components";
|
|
71
72
|
|
|
72
73
|
// Presentation - Screens
|
|
73
|
-
export { GeneratingScreen, TextInputScreen } from "./presentation/screens";
|
|
74
|
+
export { GeneratingScreen, TextInputScreen, AudioPickerScreen } from "./presentation/screens";
|
|
74
75
|
export type {
|
|
75
76
|
TextInputScreenTranslations,
|
|
76
77
|
TextInputScreenConfig,
|
|
77
78
|
TextInputScreenProps,
|
|
79
|
+
AudioPickerScreenTranslations,
|
|
80
|
+
AudioPickerScreenProps,
|
|
78
81
|
} from "./presentation/screens";
|
|
79
82
|
|
|
80
83
|
// Feature Configs
|
|
@@ -82,4 +85,5 @@ export {
|
|
|
82
85
|
TEXT_TO_IMAGE_WIZARD_CONFIG,
|
|
83
86
|
TEXT_TO_VIDEO_WIZARD_CONFIG,
|
|
84
87
|
IMAGE_TO_VIDEO_WIZARD_CONFIG,
|
|
88
|
+
SOLO_VIDEO_WIZARD_CONFIG,
|
|
85
89
|
} from "./configs";
|
|
@@ -12,6 +12,7 @@ import { renderPreviewStep } from "./step-renderers/renderPreviewStep";
|
|
|
12
12
|
import { renderPhotoUploadStep } from "./step-renderers/renderPhotoUploadStep";
|
|
13
13
|
import { renderTextInputStep } from "./step-renderers/renderTextInputStep";
|
|
14
14
|
import { renderSelectionStep } from "./step-renderers/renderSelectionStep";
|
|
15
|
+
import { renderAudioPickerStep } from "./step-renderers/renderAudioPickerStep";
|
|
15
16
|
import type { WizardStepRendererProps } from "./WizardStepRenderer.types";
|
|
16
17
|
|
|
17
18
|
export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
@@ -97,6 +98,9 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
|
97
98
|
case StepType.FEATURE_SELECTION:
|
|
98
99
|
return renderSelectionStep({ key: step.id, step, customData, onBack, onPhotoContinue, calculateCreditForSelection, t, creditCost });
|
|
99
100
|
|
|
101
|
+
case StepType.AUDIO_PICKER:
|
|
102
|
+
return renderAudioPickerStep({ key: step.id, step, onBack, onPhotoContinue, t, creditCost });
|
|
103
|
+
|
|
100
104
|
default:
|
|
101
105
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
106
|
console.warn("[WizardStepRenderer] Unhandled step type", { stepType: step.type });
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
WizardStepConfig,
|
|
3
3
|
TextInputStepConfig,
|
|
4
4
|
SelectionStepConfig,
|
|
5
|
+
AudioPickerStepConfig,
|
|
5
6
|
} from "../../domain/entities/wizard-step.types";
|
|
6
7
|
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
7
8
|
|
|
@@ -51,3 +52,9 @@ export function getTextInputValue(data: unknown): string | undefined {
|
|
|
51
52
|
if (isRecord(data) && "text" in data) return String(data.text);
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
export function getAudioPickerConfig(config: unknown): AudioPickerStepConfig | undefined {
|
|
57
|
+
if (!isRecord(config)) return undefined;
|
|
58
|
+
if (config.type === "audio_picker") return config as unknown as AudioPickerStepConfig;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Picker Step Renderer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { AudioPickerScreen } from "../../screens/AudioPickerScreen";
|
|
7
|
+
import { getAudioPickerConfig } from "../WizardStepRenderer.utils";
|
|
8
|
+
import type { StepDefinition } from "../../../../../../domain/entities/flow-config.types";
|
|
9
|
+
import type { UploadedImage } from "../../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
10
|
+
|
|
11
|
+
interface AudioPickerStepProps {
|
|
12
|
+
readonly key?: string;
|
|
13
|
+
readonly step: StepDefinition;
|
|
14
|
+
readonly onBack: () => void;
|
|
15
|
+
readonly onPhotoContinue: (stepId: string, image: UploadedImage) => void;
|
|
16
|
+
readonly t: (key: string) => string;
|
|
17
|
+
readonly creditCost?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderAudioPickerStep({
|
|
21
|
+
step,
|
|
22
|
+
onBack,
|
|
23
|
+
onPhotoContinue,
|
|
24
|
+
t,
|
|
25
|
+
creditCost,
|
|
26
|
+
}: AudioPickerStepProps): React.ReactElement {
|
|
27
|
+
const audioConfig = getAudioPickerConfig(step.config);
|
|
28
|
+
const titleKey = audioConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
|
|
29
|
+
const subtitleKey = audioConfig?.subtitleKey ?? `wizard.steps.${step.id}.subtitle`;
|
|
30
|
+
const isOptional = !(step.required ?? true);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<AudioPickerScreen
|
|
34
|
+
key={step.id}
|
|
35
|
+
stepId={step.id}
|
|
36
|
+
translations={{
|
|
37
|
+
title: t(titleKey),
|
|
38
|
+
subtitle: subtitleKey ? t(subtitleKey) : undefined,
|
|
39
|
+
selectButton: t("audioPicker.selectFile"),
|
|
40
|
+
skipButton: t("audioPicker.skip"),
|
|
41
|
+
continueButton: t("common.continue"),
|
|
42
|
+
selectedLabel: t("audioPicker.selected"),
|
|
43
|
+
fileTooLarge: t("common.errors.file_too_large"),
|
|
44
|
+
unsupportedFormat: t("audioPicker.unsupportedFormat"),
|
|
45
|
+
}}
|
|
46
|
+
allowedTypes={audioConfig?.allowedTypes as string[] | undefined}
|
|
47
|
+
maxFileSizeMB={audioConfig?.maxFileSizeMB}
|
|
48
|
+
optional={isOptional}
|
|
49
|
+
creditCost={creditCost}
|
|
50
|
+
onBack={onBack}
|
|
51
|
+
onContinue={(audioUri) => {
|
|
52
|
+
onPhotoContinue(step.id, { uri: audioUri, previewUrl: "" } as UploadedImage);
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioPickerScreen
|
|
3
|
+
* Allows users to pick an audio file (mp3, m4a, wav) for video generation.
|
|
4
|
+
* Supports optional skip. Uses expo-document-picker.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
8
|
+
import { View, StyleSheet } from "react-native";
|
|
9
|
+
import * as DocumentPicker from "expo-document-picker";
|
|
10
|
+
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
11
|
+
import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
|
|
12
|
+
import { NavigationHeader } from "@umituz/react-native-design-system/molecules";
|
|
13
|
+
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system/theme";
|
|
14
|
+
import { WizardContinueButton } from "../components/WizardContinueButton";
|
|
15
|
+
import type { AudioPickerScreenProps } from "./AudioPickerScreen.types";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
AudioPickerScreenTranslations,
|
|
19
|
+
AudioPickerScreenProps,
|
|
20
|
+
} from "./AudioPickerScreen.types";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_AUDIO_TYPES = [
|
|
23
|
+
"audio/mpeg",
|
|
24
|
+
"audio/mp4",
|
|
25
|
+
"audio/wav",
|
|
26
|
+
"audio/aac",
|
|
27
|
+
"audio/x-m4a",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const DEFAULT_MAX_SIZE_MB = 20;
|
|
31
|
+
|
|
32
|
+
export const AudioPickerScreen: React.FC<AudioPickerScreenProps> = ({
|
|
33
|
+
stepId: _stepId,
|
|
34
|
+
translations,
|
|
35
|
+
allowedTypes,
|
|
36
|
+
maxFileSizeMB,
|
|
37
|
+
optional = true,
|
|
38
|
+
creditCost,
|
|
39
|
+
onBack,
|
|
40
|
+
onContinue,
|
|
41
|
+
}) => {
|
|
42
|
+
const tokens = useAppDesignTokens();
|
|
43
|
+
const [selectedFile, setSelectedFile] = useState<{
|
|
44
|
+
uri: string;
|
|
45
|
+
name: string;
|
|
46
|
+
size?: number;
|
|
47
|
+
} | null>(null);
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
const maxSize = (maxFileSizeMB ?? DEFAULT_MAX_SIZE_MB) * 1024 * 1024;
|
|
51
|
+
const mimeTypes = allowedTypes ?? DEFAULT_AUDIO_TYPES;
|
|
52
|
+
|
|
53
|
+
const handlePick = useCallback(async () => {
|
|
54
|
+
try {
|
|
55
|
+
setError(null);
|
|
56
|
+
const result = await DocumentPicker.getDocumentAsync({
|
|
57
|
+
type: mimeTypes as string[],
|
|
58
|
+
copyToCacheDirectory: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (result.canceled || !result.assets?.length) return;
|
|
62
|
+
|
|
63
|
+
const asset = result.assets[0];
|
|
64
|
+
|
|
65
|
+
if (asset.size && asset.size > maxSize) {
|
|
66
|
+
setError(
|
|
67
|
+
translations.fileTooLarge ??
|
|
68
|
+
`File too large. Max ${maxFileSizeMB ?? DEFAULT_MAX_SIZE_MB}MB.`
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setSelectedFile({
|
|
74
|
+
uri: asset.uri,
|
|
75
|
+
name: asset.name,
|
|
76
|
+
size: asset.size ?? undefined,
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
if (__DEV__) {
|
|
80
|
+
console.warn("[AudioPickerScreen] Failed to pick document");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [mimeTypes, maxSize, maxFileSizeMB, translations]);
|
|
84
|
+
|
|
85
|
+
const handleContinue = useCallback(() => {
|
|
86
|
+
onContinue(selectedFile?.uri ?? "");
|
|
87
|
+
}, [selectedFile, onContinue]);
|
|
88
|
+
|
|
89
|
+
const handleSkip = useCallback(() => {
|
|
90
|
+
onContinue("");
|
|
91
|
+
}, [onContinue]);
|
|
92
|
+
|
|
93
|
+
const formatFileSize = useCallback((bytes?: number) => {
|
|
94
|
+
if (!bytes) return "";
|
|
95
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
96
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
97
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const canContinue = !!selectedFile || optional;
|
|
101
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View style={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
|
|
105
|
+
<NavigationHeader
|
|
106
|
+
title=""
|
|
107
|
+
onBackPress={onBack}
|
|
108
|
+
rightElement={
|
|
109
|
+
<WizardContinueButton
|
|
110
|
+
label={translations.continueButton}
|
|
111
|
+
canContinue={canContinue}
|
|
112
|
+
onPress={handleContinue}
|
|
113
|
+
creditCost={creditCost}
|
|
114
|
+
/>
|
|
115
|
+
}
|
|
116
|
+
/>
|
|
117
|
+
<ScreenLayout
|
|
118
|
+
scrollable={true}
|
|
119
|
+
edges={["left", "right"]}
|
|
120
|
+
hideScrollIndicator={true}
|
|
121
|
+
contentContainerStyle={styles.scrollContent}
|
|
122
|
+
>
|
|
123
|
+
<AtomicText type="headlineMedium" color="textPrimary" style={styles.title}>
|
|
124
|
+
{translations.title}
|
|
125
|
+
</AtomicText>
|
|
126
|
+
|
|
127
|
+
{translations.subtitle ? (
|
|
128
|
+
<AtomicText type="bodyMedium" color="textSecondary" style={styles.subtitle}>
|
|
129
|
+
{translations.subtitle}
|
|
130
|
+
</AtomicText>
|
|
131
|
+
) : null}
|
|
132
|
+
|
|
133
|
+
{selectedFile ? (
|
|
134
|
+
<View style={styles.selectedContainer}>
|
|
135
|
+
<AtomicText type="labelLarge" color="textPrimary" style={styles.fileName}>
|
|
136
|
+
{selectedFile.name}
|
|
137
|
+
</AtomicText>
|
|
138
|
+
{selectedFile.size ? (
|
|
139
|
+
<AtomicText type="bodySmall" color="textTertiary">
|
|
140
|
+
{formatFileSize(selectedFile.size)}
|
|
141
|
+
</AtomicText>
|
|
142
|
+
) : null}
|
|
143
|
+
<AtomicButton
|
|
144
|
+
variant="outline"
|
|
145
|
+
size="sm"
|
|
146
|
+
onPress={handlePick}
|
|
147
|
+
style={styles.changeButton}
|
|
148
|
+
>
|
|
149
|
+
{translations.selectButton}
|
|
150
|
+
</AtomicButton>
|
|
151
|
+
</View>
|
|
152
|
+
) : (
|
|
153
|
+
<AtomicButton
|
|
154
|
+
variant="outline"
|
|
155
|
+
size="md"
|
|
156
|
+
onPress={handlePick}
|
|
157
|
+
style={styles.pickButton}
|
|
158
|
+
>
|
|
159
|
+
{translations.selectButton}
|
|
160
|
+
</AtomicButton>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{error ? (
|
|
164
|
+
<AtomicText type="bodySmall" color="error" style={styles.error}>
|
|
165
|
+
{error}
|
|
166
|
+
</AtomicText>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
169
|
+
{optional && !selectedFile ? (
|
|
170
|
+
<AtomicButton
|
|
171
|
+
variant="ghost"
|
|
172
|
+
size="sm"
|
|
173
|
+
onPress={handleSkip}
|
|
174
|
+
style={styles.skipButton}
|
|
175
|
+
>
|
|
176
|
+
{translations.skipButton}
|
|
177
|
+
</AtomicButton>
|
|
178
|
+
) : null}
|
|
179
|
+
</ScreenLayout>
|
|
180
|
+
</View>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const createStyles = (tokens: DesignTokens) =>
|
|
185
|
+
StyleSheet.create({
|
|
186
|
+
scrollContent: {
|
|
187
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
188
|
+
paddingBottom: 40,
|
|
189
|
+
},
|
|
190
|
+
title: {
|
|
191
|
+
marginBottom: tokens.spacing.sm,
|
|
192
|
+
},
|
|
193
|
+
subtitle: {
|
|
194
|
+
marginBottom: tokens.spacing.xl,
|
|
195
|
+
},
|
|
196
|
+
selectedContainer: {
|
|
197
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
198
|
+
borderRadius: tokens.borders.radius.md,
|
|
199
|
+
borderWidth: 1,
|
|
200
|
+
borderColor: tokens.colors.border,
|
|
201
|
+
padding: tokens.spacing.lg,
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
gap: tokens.spacing.sm,
|
|
204
|
+
},
|
|
205
|
+
fileName: {
|
|
206
|
+
textAlign: "center",
|
|
207
|
+
},
|
|
208
|
+
changeButton: {
|
|
209
|
+
marginTop: tokens.spacing.sm,
|
|
210
|
+
},
|
|
211
|
+
pickButton: {
|
|
212
|
+
marginTop: tokens.spacing.md,
|
|
213
|
+
},
|
|
214
|
+
error: {
|
|
215
|
+
marginTop: tokens.spacing.sm,
|
|
216
|
+
textAlign: "center",
|
|
217
|
+
},
|
|
218
|
+
skipButton: {
|
|
219
|
+
marginTop: tokens.spacing.lg,
|
|
220
|
+
alignSelf: "center",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioPickerScreen Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface AudioPickerScreenTranslations {
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly subtitle?: string;
|
|
8
|
+
readonly selectButton: string;
|
|
9
|
+
readonly skipButton: string;
|
|
10
|
+
readonly continueButton: string;
|
|
11
|
+
readonly selectedLabel: string;
|
|
12
|
+
readonly fileTooLarge?: string;
|
|
13
|
+
readonly unsupportedFormat?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AudioPickerScreenProps {
|
|
17
|
+
readonly stepId: string;
|
|
18
|
+
readonly translations: AudioPickerScreenTranslations;
|
|
19
|
+
/** Allowed MIME types */
|
|
20
|
+
readonly allowedTypes?: readonly string[];
|
|
21
|
+
/** Max file size in MB */
|
|
22
|
+
readonly maxFileSizeMB?: number;
|
|
23
|
+
/** Whether this step can be skipped */
|
|
24
|
+
readonly optional?: boolean;
|
|
25
|
+
/** Calculated credit cost from parent */
|
|
26
|
+
readonly creditCost?: number;
|
|
27
|
+
readonly onBack: () => void;
|
|
28
|
+
/** Called with audio URI, or empty string if skipped */
|
|
29
|
+
readonly onContinue: (audioUri: string) => void;
|
|
30
|
+
}
|
|
@@ -25,6 +25,7 @@ export const ResultActionBar: React.FC<ResultActionBarProps> = ({
|
|
|
25
25
|
showRating = false,
|
|
26
26
|
onEdit,
|
|
27
27
|
onEditVideo,
|
|
28
|
+
onShareToFeed,
|
|
28
29
|
}) => {
|
|
29
30
|
const tokens = useAppDesignTokens();
|
|
30
31
|
const { minTouchTarget } = useResponsive();
|
|
@@ -134,6 +135,15 @@ export const ResultActionBar: React.FC<ResultActionBarProps> = ({
|
|
|
134
135
|
<AtomicIcon name="video" customSize={20} color="onPrimary" />
|
|
135
136
|
</TouchableOpacity>
|
|
136
137
|
)}
|
|
138
|
+
{onShareToFeed && (
|
|
139
|
+
<TouchableOpacity
|
|
140
|
+
style={styles.iconButton}
|
|
141
|
+
onPress={onShareToFeed}
|
|
142
|
+
activeOpacity={0.7}
|
|
143
|
+
>
|
|
144
|
+
<AtomicIcon name="send" customSize={20} color="onPrimary" />
|
|
145
|
+
</TouchableOpacity>
|
|
146
|
+
)}
|
|
137
147
|
</View>
|
|
138
148
|
);
|
|
139
149
|
}
|
|
@@ -23,6 +23,7 @@ export const ResultPreviewScreen: React.FC<ResultPreviewScreenProps> = ({
|
|
|
23
23
|
onRate,
|
|
24
24
|
onEdit,
|
|
25
25
|
onEditVideo,
|
|
26
|
+
onShareToFeed,
|
|
26
27
|
recentCreations,
|
|
27
28
|
onViewAll,
|
|
28
29
|
onCreationPress,
|
|
@@ -75,6 +76,7 @@ export const ResultPreviewScreen: React.FC<ResultPreviewScreenProps> = ({
|
|
|
75
76
|
onRate={onRate}
|
|
76
77
|
onEdit={onEdit}
|
|
77
78
|
onEditVideo={onEditVideo}
|
|
79
|
+
onShareToFeed={onShareToFeed}
|
|
78
80
|
saveButtonText={translations.saveButton}
|
|
79
81
|
shareButtonText={translations.shareButton}
|
|
80
82
|
tryAgainButtonText={translations.tryAnother}
|
|
@@ -48,4 +48,6 @@ export interface ResultActionBarProps {
|
|
|
48
48
|
onEdit?: () => void;
|
|
49
49
|
/** Edit video button callback — only shown in iconOnly mode when provided */
|
|
50
50
|
onEditVideo?: () => void;
|
|
51
|
+
/** Post to feed callback — when provided, shows a "send" button. Omit to hide (apps without a feed). */
|
|
52
|
+
onShareToFeed?: () => void;
|
|
51
53
|
}
|
|
@@ -26,6 +26,8 @@ export interface ResultPreviewScreenProps {
|
|
|
26
26
|
onEdit?: () => void;
|
|
27
27
|
/** Edit video callback — opens video editor for the result video */
|
|
28
28
|
onEditVideo?: () => void;
|
|
29
|
+
/** Post to feed callback — when provided, shows a send button. Omit to hide (apps without a feed). */
|
|
30
|
+
onShareToFeed?: () => void;
|
|
29
31
|
/** Recent creations to display */
|
|
30
32
|
recentCreations?: readonly RecentCreation[];
|
|
31
33
|
/** Navigate to all creations */
|
package/src/index.ts
CHANGED