@umituz/react-native-ai-generation-content 1.17.142 → 1.17.144
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 +1 -1
- package/src/domains/creations/presentation/hooks/index.ts +2 -2
- package/src/domains/creations/presentation/hooks/{useMediaFilter.ts → useFilter.ts} +12 -7
- package/src/domains/creations/presentation/hooks/useGalleryFilters.ts +5 -6
- package/src/features/ai-hug/presentation/hooks/useAIHugFeature.ts +9 -113
- package/src/features/ai-kiss/presentation/hooks/useAIKissFeature.ts +9 -113
- package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +10 -7
- package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +5 -0
- package/src/features/shared/dual-image-video/domain/types/dual-image-video.types.ts +67 -0
- package/src/features/shared/dual-image-video/domain/types/index.ts +13 -0
- package/src/features/shared/dual-image-video/index.ts +16 -0
- package/src/features/shared/dual-image-video/presentation/hooks/index.ts +5 -0
- package/src/features/shared/dual-image-video/presentation/hooks/useDualImageVideoFeature.ts +124 -0
- package/src/features/shared/index.ts +6 -0
- package/src/infrastructure/utils/progress-calculator.util.ts +31 -0
- package/src/domains/creations/presentation/hooks/useStatusFilter.ts +0 -54
package/package.json
CHANGED
|
@@ -6,6 +6,6 @@ export { useCreations } from "./useCreations";
|
|
|
6
6
|
export { useDeleteCreation } from "./useDeleteCreation";
|
|
7
7
|
export { useCreationsFilter } from "./useCreationsFilter";
|
|
8
8
|
export { useAdvancedFilter } from "./useAdvancedFilter";
|
|
9
|
-
export { useStatusFilter } from "./
|
|
10
|
-
export {
|
|
9
|
+
export { useFilter, useStatusFilter, useMediaFilter } from "./useFilter";
|
|
10
|
+
export type { UseFilterProps, UseFilterReturn } from "./useFilter";
|
|
11
11
|
export { useGalleryFilters } from "./useGalleryFilters";
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* SOLID: Single Responsibility - Only handles
|
|
2
|
+
* useFilter Hook
|
|
3
|
+
* Generic filter state management for any filter type
|
|
4
|
+
* SOLID: Single Responsibility - Only handles filter state
|
|
5
|
+
* DRY: Replaces duplicate useMediaFilter and useStatusFilter
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { useState, useCallback, useMemo } from "react";
|
|
8
9
|
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
9
10
|
|
|
10
|
-
interface
|
|
11
|
+
export interface UseFilterProps {
|
|
11
12
|
readonly options: FilterOption[];
|
|
12
13
|
readonly t: (key: string) => string;
|
|
13
14
|
readonly defaultId?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
interface
|
|
17
|
+
export interface UseFilterReturn {
|
|
17
18
|
readonly selectedId: string;
|
|
18
19
|
readonly filterOptions: FilterOption[];
|
|
19
20
|
readonly hasActiveFilter: boolean;
|
|
@@ -21,11 +22,11 @@ interface UseMediaFilterReturn {
|
|
|
21
22
|
readonly clearFilter: () => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export function
|
|
25
|
+
export function useFilter({
|
|
25
26
|
options,
|
|
26
27
|
t,
|
|
27
28
|
defaultId = "all"
|
|
28
|
-
}:
|
|
29
|
+
}: UseFilterProps): UseFilterReturn {
|
|
29
30
|
const [selectedId, setSelectedId] = useState(defaultId);
|
|
30
31
|
|
|
31
32
|
const filterOptions = useMemo(() =>
|
|
@@ -52,3 +53,7 @@ export function useMediaFilter({
|
|
|
52
53
|
clearFilter
|
|
53
54
|
};
|
|
54
55
|
}
|
|
56
|
+
|
|
57
|
+
// Backward compatibility aliases
|
|
58
|
+
export const useMediaFilter = useFilter;
|
|
59
|
+
export const useStatusFilter = useFilter;
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
import { useState, useCallback } from "react";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
10
|
-
import {
|
|
11
|
-
import { useMediaFilter } from "./useMediaFilter";
|
|
10
|
+
import { useFilter } from "./useFilter";
|
|
12
11
|
import { useCreationsFilter } from "./useCreationsFilter";
|
|
13
12
|
|
|
14
13
|
interface UseGalleryFiltersProps {
|
|
@@ -24,8 +23,8 @@ interface UseGalleryFiltersReturn {
|
|
|
24
23
|
readonly activeFiltersCount: number;
|
|
25
24
|
readonly statusFilterVisible: boolean;
|
|
26
25
|
readonly mediaFilterVisible: boolean;
|
|
27
|
-
readonly statusFilter: ReturnType<typeof
|
|
28
|
-
readonly mediaFilter: ReturnType<typeof
|
|
26
|
+
readonly statusFilter: ReturnType<typeof useFilter>;
|
|
27
|
+
readonly mediaFilter: ReturnType<typeof useFilter>;
|
|
29
28
|
readonly openStatusFilter: () => void;
|
|
30
29
|
readonly closeStatusFilter: () => void;
|
|
31
30
|
readonly openMediaFilter: () => void;
|
|
@@ -42,8 +41,8 @@ export function useGalleryFilters({
|
|
|
42
41
|
const [statusFilterVisible, setStatusFilterVisible] = useState(false);
|
|
43
42
|
const [mediaFilterVisible, setMediaFilterVisible] = useState(false);
|
|
44
43
|
|
|
45
|
-
const statusFilter =
|
|
46
|
-
const mediaFilter =
|
|
44
|
+
const statusFilter = useFilter({ options: statusOptions, t });
|
|
45
|
+
const mediaFilter = useFilter({ options: mediaOptions, t });
|
|
47
46
|
|
|
48
47
|
const { filtered, isFiltered, activeFiltersCount } = useCreationsFilter({
|
|
49
48
|
creations,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAIHugFeature Hook
|
|
3
|
-
*
|
|
3
|
+
* Thin wrapper around useDualImageVideoFeature for ai-hug feature
|
|
4
|
+
* DRY: Uses shared base hook for common logic
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import { executeVideoFeature } from "../../../../infrastructure/services";
|
|
7
|
+
import { useDualImageVideoFeature } from "../../../shared/dual-image-video";
|
|
8
8
|
import type {
|
|
9
|
-
AIHugFeatureState,
|
|
10
9
|
AIHugFeatureConfig,
|
|
10
|
+
AIHugFeatureState,
|
|
11
11
|
} from "../../domain/types";
|
|
12
12
|
|
|
13
13
|
export interface UseAIHugFeatureProps {
|
|
@@ -25,113 +25,9 @@ export interface UseAIHugFeatureReturn extends AIHugFeatureState {
|
|
|
25
25
|
reset: () => void;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
progress: 0,
|
|
34
|
-
error: null,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export function useAIHugFeature(
|
|
38
|
-
props: UseAIHugFeatureProps,
|
|
39
|
-
): UseAIHugFeatureReturn {
|
|
40
|
-
const { config, onSelectSourceImage, onSelectTargetImage, onSaveVideo } = props;
|
|
41
|
-
const [state, setState] = useState<AIHugFeatureState>(initialState);
|
|
42
|
-
|
|
43
|
-
const selectSourceImage = useCallback(async () => {
|
|
44
|
-
try {
|
|
45
|
-
const uri = await onSelectSourceImage();
|
|
46
|
-
if (uri) {
|
|
47
|
-
setState((prev) => ({ ...prev, sourceImageUri: uri, error: null }));
|
|
48
|
-
config.onSourceImageSelect?.(uri);
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
53
|
-
}
|
|
54
|
-
}, [onSelectSourceImage, config]);
|
|
55
|
-
|
|
56
|
-
const selectTargetImage = useCallback(async () => {
|
|
57
|
-
try {
|
|
58
|
-
const uri = await onSelectTargetImage();
|
|
59
|
-
if (uri) {
|
|
60
|
-
setState((prev) => ({ ...prev, targetImageUri: uri, error: null }));
|
|
61
|
-
config.onTargetImageSelect?.(uri);
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
66
|
-
}
|
|
67
|
-
}, [onSelectTargetImage, config]);
|
|
68
|
-
|
|
69
|
-
const handleProgress = useCallback((progress: number) => {
|
|
70
|
-
setState((prev) => ({ ...prev, progress }));
|
|
71
|
-
}, []);
|
|
72
|
-
|
|
73
|
-
const process = useCallback(async () => {
|
|
74
|
-
if (!state.sourceImageUri || !state.targetImageUri) return;
|
|
75
|
-
|
|
76
|
-
setState((prev) => ({
|
|
77
|
-
...prev,
|
|
78
|
-
isProcessing: true,
|
|
79
|
-
progress: 0,
|
|
80
|
-
error: null,
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
config.onProcessingStart?.();
|
|
84
|
-
|
|
85
|
-
const sourceImageBase64 = await config.prepareImage(state.sourceImageUri);
|
|
86
|
-
const targetImageBase64 = await config.prepareImage(state.targetImageUri);
|
|
87
|
-
|
|
88
|
-
const result = await executeVideoFeature(
|
|
89
|
-
"ai-hug",
|
|
90
|
-
{ sourceImageBase64, targetImageBase64 },
|
|
91
|
-
{ extractResult: config.extractResult, onProgress: handleProgress },
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
if (result.success && result.videoUrl) {
|
|
95
|
-
setState((prev) => ({
|
|
96
|
-
...prev,
|
|
97
|
-
isProcessing: false,
|
|
98
|
-
processedVideoUrl: result.videoUrl!,
|
|
99
|
-
progress: 100,
|
|
100
|
-
}));
|
|
101
|
-
config.onProcessingComplete?.({ success: true, videoUrl: result.videoUrl });
|
|
102
|
-
} else {
|
|
103
|
-
const errorMessage = result.error || "Processing failed";
|
|
104
|
-
setState((prev) => ({
|
|
105
|
-
...prev,
|
|
106
|
-
isProcessing: false,
|
|
107
|
-
error: errorMessage,
|
|
108
|
-
progress: 0,
|
|
109
|
-
}));
|
|
110
|
-
config.onError?.(errorMessage);
|
|
111
|
-
}
|
|
112
|
-
}, [state.sourceImageUri, state.targetImageUri, config, handleProgress]);
|
|
113
|
-
|
|
114
|
-
const save = useCallback(async () => {
|
|
115
|
-
if (!state.processedVideoUrl) return;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
await onSaveVideo(state.processedVideoUrl);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
122
|
-
}
|
|
123
|
-
}, [state.processedVideoUrl, onSaveVideo]);
|
|
124
|
-
|
|
125
|
-
const reset = useCallback(() => {
|
|
126
|
-
setState(initialState);
|
|
127
|
-
}, []);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
...state,
|
|
131
|
-
selectSourceImage,
|
|
132
|
-
selectTargetImage,
|
|
133
|
-
process,
|
|
134
|
-
save,
|
|
135
|
-
reset,
|
|
136
|
-
};
|
|
28
|
+
export function useAIHugFeature(props: UseAIHugFeatureProps): UseAIHugFeatureReturn {
|
|
29
|
+
return useDualImageVideoFeature({
|
|
30
|
+
featureType: "ai-hug",
|
|
31
|
+
...props,
|
|
32
|
+
});
|
|
137
33
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAIKissFeature Hook
|
|
3
|
-
*
|
|
3
|
+
* Thin wrapper around useDualImageVideoFeature for ai-kiss feature
|
|
4
|
+
* DRY: Uses shared base hook for common logic
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import { executeVideoFeature } from "../../../../infrastructure/services";
|
|
7
|
+
import { useDualImageVideoFeature } from "../../../shared/dual-image-video";
|
|
8
8
|
import type {
|
|
9
|
-
AIKissFeatureState,
|
|
10
9
|
AIKissFeatureConfig,
|
|
10
|
+
AIKissFeatureState,
|
|
11
11
|
} from "../../domain/types";
|
|
12
12
|
|
|
13
13
|
export interface UseAIKissFeatureProps {
|
|
@@ -25,113 +25,9 @@ export interface UseAIKissFeatureReturn extends AIKissFeatureState {
|
|
|
25
25
|
reset: () => void;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
progress: 0,
|
|
34
|
-
error: null,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export function useAIKissFeature(
|
|
38
|
-
props: UseAIKissFeatureProps,
|
|
39
|
-
): UseAIKissFeatureReturn {
|
|
40
|
-
const { config, onSelectSourceImage, onSelectTargetImage, onSaveVideo } = props;
|
|
41
|
-
const [state, setState] = useState<AIKissFeatureState>(initialState);
|
|
42
|
-
|
|
43
|
-
const selectSourceImage = useCallback(async () => {
|
|
44
|
-
try {
|
|
45
|
-
const uri = await onSelectSourceImage();
|
|
46
|
-
if (uri) {
|
|
47
|
-
setState((prev) => ({ ...prev, sourceImageUri: uri, error: null }));
|
|
48
|
-
config.onSourceImageSelect?.(uri);
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
53
|
-
}
|
|
54
|
-
}, [onSelectSourceImage, config]);
|
|
55
|
-
|
|
56
|
-
const selectTargetImage = useCallback(async () => {
|
|
57
|
-
try {
|
|
58
|
-
const uri = await onSelectTargetImage();
|
|
59
|
-
if (uri) {
|
|
60
|
-
setState((prev) => ({ ...prev, targetImageUri: uri, error: null }));
|
|
61
|
-
config.onTargetImageSelect?.(uri);
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
66
|
-
}
|
|
67
|
-
}, [onSelectTargetImage, config]);
|
|
68
|
-
|
|
69
|
-
const handleProgress = useCallback((progress: number) => {
|
|
70
|
-
setState((prev) => ({ ...prev, progress }));
|
|
71
|
-
}, []);
|
|
72
|
-
|
|
73
|
-
const process = useCallback(async () => {
|
|
74
|
-
if (!state.sourceImageUri || !state.targetImageUri) return;
|
|
75
|
-
|
|
76
|
-
setState((prev) => ({
|
|
77
|
-
...prev,
|
|
78
|
-
isProcessing: true,
|
|
79
|
-
progress: 0,
|
|
80
|
-
error: null,
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
config.onProcessingStart?.();
|
|
84
|
-
|
|
85
|
-
const sourceImageBase64 = await config.prepareImage(state.sourceImageUri);
|
|
86
|
-
const targetImageBase64 = await config.prepareImage(state.targetImageUri);
|
|
87
|
-
|
|
88
|
-
const result = await executeVideoFeature(
|
|
89
|
-
"ai-kiss",
|
|
90
|
-
{ sourceImageBase64, targetImageBase64 },
|
|
91
|
-
{ extractResult: config.extractResult, onProgress: handleProgress },
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
if (result.success && result.videoUrl) {
|
|
95
|
-
setState((prev) => ({
|
|
96
|
-
...prev,
|
|
97
|
-
isProcessing: false,
|
|
98
|
-
processedVideoUrl: result.videoUrl!,
|
|
99
|
-
progress: 100,
|
|
100
|
-
}));
|
|
101
|
-
config.onProcessingComplete?.({ success: true, videoUrl: result.videoUrl });
|
|
102
|
-
} else {
|
|
103
|
-
const errorMessage = result.error || "Processing failed";
|
|
104
|
-
setState((prev) => ({
|
|
105
|
-
...prev,
|
|
106
|
-
isProcessing: false,
|
|
107
|
-
error: errorMessage,
|
|
108
|
-
progress: 0,
|
|
109
|
-
}));
|
|
110
|
-
config.onError?.(errorMessage);
|
|
111
|
-
}
|
|
112
|
-
}, [state.sourceImageUri, state.targetImageUri, config, handleProgress]);
|
|
113
|
-
|
|
114
|
-
const save = useCallback(async () => {
|
|
115
|
-
if (!state.processedVideoUrl) return;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
await onSaveVideo(state.processedVideoUrl);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
122
|
-
}
|
|
123
|
-
}, [state.processedVideoUrl, onSaveVideo]);
|
|
124
|
-
|
|
125
|
-
const reset = useCallback(() => {
|
|
126
|
-
setState(initialState);
|
|
127
|
-
}, []);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
...state,
|
|
131
|
-
selectSourceImage,
|
|
132
|
-
selectTargetImage,
|
|
133
|
-
process,
|
|
134
|
-
save,
|
|
135
|
-
reset,
|
|
136
|
-
};
|
|
28
|
+
export function useAIKissFeature(props: UseAIKissFeatureProps): UseAIKissFeatureReturn {
|
|
29
|
+
return useDualImageVideoFeature({
|
|
30
|
+
featureType: "ai-kiss",
|
|
31
|
+
...props,
|
|
32
|
+
});
|
|
137
33
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Image-to-Video Executor
|
|
3
3
|
* Provider-agnostic image-to-video execution using active AI provider
|
|
4
|
-
*
|
|
4
|
+
* Uses progress mapper for consistent progress reporting
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { providerRegistry } from "../../../../infrastructure/services";
|
|
8
|
+
import { getProgressFromJobStatus } from "../../../../infrastructure/utils";
|
|
8
9
|
import type {
|
|
9
10
|
ImageToVideoRequest,
|
|
10
11
|
ImageToVideoResult,
|
|
@@ -103,16 +104,18 @@ export async function executeImageToVideo(
|
|
|
103
104
|
// eslint-disable-next-line no-console
|
|
104
105
|
console.log("[ImageToVideoExecutor] Queue status:", status.status, "position:", status.queuePosition);
|
|
105
106
|
}
|
|
106
|
-
// Map
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} else if (status.status === "IN_PROGRESS") {
|
|
110
|
-
onProgress?.(50);
|
|
111
|
-
}
|
|
107
|
+
// Map provider status to progress using centralized mapper
|
|
108
|
+
const progress = getProgressFromJobStatus(status.status);
|
|
109
|
+
onProgress?.(progress);
|
|
112
110
|
},
|
|
113
111
|
timeoutMs: 300000, // 5 minutes timeout for video generation
|
|
114
112
|
});
|
|
115
113
|
|
|
114
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log("[ImageToVideoExecutor] Subscribe resolved, result keys:", result ? Object.keys(result as object) : "null");
|
|
117
|
+
}
|
|
118
|
+
|
|
116
119
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
117
120
|
// eslint-disable-next-line no-console
|
|
118
121
|
console.log("[ImageToVideoExecutor] provider.subscribe() completed");
|
|
@@ -63,27 +63,32 @@ export function useImageToVideoForm(
|
|
|
63
63
|
|
|
64
64
|
const handleSelectImages = useCallback(async () => {
|
|
65
65
|
if (__DEV__) {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
66
67
|
console.log("[useImageToVideoForm] handleSelectImages called");
|
|
67
68
|
}
|
|
68
69
|
if (callbacks.onSelectImages) {
|
|
69
70
|
try {
|
|
70
71
|
const images = await callbacks.onSelectImages();
|
|
71
72
|
if (__DEV__) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
72
74
|
console.log("[useImageToVideoForm] Images selected:", images.length);
|
|
73
75
|
}
|
|
74
76
|
if (images.length > 0) {
|
|
75
77
|
actions.addImages(images);
|
|
76
78
|
if (__DEV__) {
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
77
80
|
console.log("[useImageToVideoForm] Images added to state");
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
} catch (error) {
|
|
81
84
|
if (__DEV__) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
82
86
|
console.error("[useImageToVideoForm] Error selecting images:", error);
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
} else {
|
|
86
90
|
if (__DEV__) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
87
92
|
console.warn("[useImageToVideoForm] No onSelectImages callback provided");
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual Image Video Feature Types
|
|
3
|
+
* Shared types for video features that take two images (ai-hug, ai-kiss, etc.)
|
|
4
|
+
* DRY: Consolidates common types from ai-hug and ai-kiss features
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VideoFeatureType } from "../../../../../domain/interfaces/ai-provider.interface";
|
|
8
|
+
|
|
9
|
+
export interface DualImageVideoFeatureState {
|
|
10
|
+
sourceImageUri: string | null;
|
|
11
|
+
targetImageUri: string | null;
|
|
12
|
+
processedVideoUrl: string | null;
|
|
13
|
+
isProcessing: boolean;
|
|
14
|
+
progress: number;
|
|
15
|
+
error: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DualImageVideoResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
videoUrl?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
requestId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type DualImageVideoResultExtractor = (result: unknown) => string | undefined;
|
|
26
|
+
|
|
27
|
+
export interface DualImageVideoFeatureConfig {
|
|
28
|
+
creditCost?: number;
|
|
29
|
+
extractResult?: DualImageVideoResultExtractor;
|
|
30
|
+
prepareImage: (imageUri: string) => Promise<string>;
|
|
31
|
+
onSourceImageSelect?: (uri: string) => void;
|
|
32
|
+
onTargetImageSelect?: (uri: string) => void;
|
|
33
|
+
onProcessingStart?: () => void;
|
|
34
|
+
onProcessingComplete?: (result: DualImageVideoResult) => void;
|
|
35
|
+
onError?: (error: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DualImageVideoTranslations {
|
|
39
|
+
sourceUploadTitle: string;
|
|
40
|
+
sourceUploadSubtitle: string;
|
|
41
|
+
targetUploadTitle: string;
|
|
42
|
+
targetUploadSubtitle: string;
|
|
43
|
+
uploadChange: string;
|
|
44
|
+
uploadAnalyzing: string;
|
|
45
|
+
description: string;
|
|
46
|
+
processingText: string;
|
|
47
|
+
processButtonText: string;
|
|
48
|
+
successText: string;
|
|
49
|
+
saveButtonText: string;
|
|
50
|
+
tryAnotherText: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface UseDualImageVideoFeatureProps {
|
|
54
|
+
featureType: VideoFeatureType;
|
|
55
|
+
config: DualImageVideoFeatureConfig;
|
|
56
|
+
onSelectSourceImage: () => Promise<string | null>;
|
|
57
|
+
onSelectTargetImage: () => Promise<string | null>;
|
|
58
|
+
onSaveVideo: (videoUrl: string) => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface UseDualImageVideoFeatureReturn extends DualImageVideoFeatureState {
|
|
62
|
+
selectSourceImage: () => Promise<void>;
|
|
63
|
+
selectTargetImage: () => Promise<void>;
|
|
64
|
+
process: () => Promise<void>;
|
|
65
|
+
save: () => Promise<void>;
|
|
66
|
+
reset: () => void;
|
|
67
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual Image Video Domain Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
DualImageVideoFeatureState,
|
|
7
|
+
DualImageVideoResult,
|
|
8
|
+
DualImageVideoResultExtractor,
|
|
9
|
+
DualImageVideoFeatureConfig,
|
|
10
|
+
DualImageVideoTranslations,
|
|
11
|
+
UseDualImageVideoFeatureProps,
|
|
12
|
+
UseDualImageVideoFeatureReturn,
|
|
13
|
+
} from "./dual-image-video.types";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual Image Video Feature (Shared)
|
|
3
|
+
* Base infrastructure for video features using two source images
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
DualImageVideoFeatureState,
|
|
8
|
+
DualImageVideoResult,
|
|
9
|
+
DualImageVideoResultExtractor,
|
|
10
|
+
DualImageVideoFeatureConfig,
|
|
11
|
+
DualImageVideoTranslations,
|
|
12
|
+
UseDualImageVideoFeatureProps,
|
|
13
|
+
UseDualImageVideoFeatureReturn,
|
|
14
|
+
} from "./domain/types";
|
|
15
|
+
|
|
16
|
+
export { useDualImageVideoFeature } from "./presentation/hooks";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDualImageVideoFeature Hook
|
|
3
|
+
* Base hook for video features that take two images (ai-hug, ai-kiss, etc.)
|
|
4
|
+
* DRY: Consolidates common logic from useAIHugFeature and useAIKissFeature
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
import { executeVideoFeature } from "../../../../../infrastructure/services";
|
|
9
|
+
import type {
|
|
10
|
+
DualImageVideoFeatureState,
|
|
11
|
+
UseDualImageVideoFeatureProps,
|
|
12
|
+
UseDualImageVideoFeatureReturn,
|
|
13
|
+
} from "../../domain/types/dual-image-video.types";
|
|
14
|
+
|
|
15
|
+
const initialState: DualImageVideoFeatureState = {
|
|
16
|
+
sourceImageUri: null,
|
|
17
|
+
targetImageUri: null,
|
|
18
|
+
processedVideoUrl: null,
|
|
19
|
+
isProcessing: false,
|
|
20
|
+
progress: 0,
|
|
21
|
+
error: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function useDualImageVideoFeature(
|
|
25
|
+
props: UseDualImageVideoFeatureProps,
|
|
26
|
+
): UseDualImageVideoFeatureReturn {
|
|
27
|
+
const { featureType, config, onSelectSourceImage, onSelectTargetImage, onSaveVideo } = props;
|
|
28
|
+
const [state, setState] = useState<DualImageVideoFeatureState>(initialState);
|
|
29
|
+
|
|
30
|
+
const selectSourceImage = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const uri = await onSelectSourceImage();
|
|
33
|
+
if (uri) {
|
|
34
|
+
setState((prev) => ({ ...prev, sourceImageUri: uri, error: null }));
|
|
35
|
+
config.onSourceImageSelect?.(uri);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
setState((prev) => ({ ...prev, error: message }));
|
|
40
|
+
}
|
|
41
|
+
}, [onSelectSourceImage, config]);
|
|
42
|
+
|
|
43
|
+
const selectTargetImage = useCallback(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const uri = await onSelectTargetImage();
|
|
46
|
+
if (uri) {
|
|
47
|
+
setState((prev) => ({ ...prev, targetImageUri: uri, error: null }));
|
|
48
|
+
config.onTargetImageSelect?.(uri);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
setState((prev) => ({ ...prev, error: message }));
|
|
53
|
+
}
|
|
54
|
+
}, [onSelectTargetImage, config]);
|
|
55
|
+
|
|
56
|
+
const handleProgress = useCallback((progress: number) => {
|
|
57
|
+
setState((prev) => ({ ...prev, progress }));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const process = useCallback(async () => {
|
|
61
|
+
if (!state.sourceImageUri || !state.targetImageUri) return;
|
|
62
|
+
|
|
63
|
+
setState((prev) => ({
|
|
64
|
+
...prev,
|
|
65
|
+
isProcessing: true,
|
|
66
|
+
progress: 0,
|
|
67
|
+
error: null,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
config.onProcessingStart?.();
|
|
71
|
+
|
|
72
|
+
const sourceImageBase64 = await config.prepareImage(state.sourceImageUri);
|
|
73
|
+
const targetImageBase64 = await config.prepareImage(state.targetImageUri);
|
|
74
|
+
|
|
75
|
+
const result = await executeVideoFeature(
|
|
76
|
+
featureType,
|
|
77
|
+
{ sourceImageBase64, targetImageBase64 },
|
|
78
|
+
{ extractResult: config.extractResult, onProgress: handleProgress },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (result.success && result.videoUrl) {
|
|
82
|
+
setState((prev) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
isProcessing: false,
|
|
85
|
+
processedVideoUrl: result.videoUrl!,
|
|
86
|
+
progress: 100,
|
|
87
|
+
}));
|
|
88
|
+
config.onProcessingComplete?.({ success: true, videoUrl: result.videoUrl });
|
|
89
|
+
} else {
|
|
90
|
+
const errorMessage = result.error || "Processing failed";
|
|
91
|
+
setState((prev) => ({
|
|
92
|
+
...prev,
|
|
93
|
+
isProcessing: false,
|
|
94
|
+
error: errorMessage,
|
|
95
|
+
progress: 0,
|
|
96
|
+
}));
|
|
97
|
+
config.onError?.(errorMessage);
|
|
98
|
+
}
|
|
99
|
+
}, [state.sourceImageUri, state.targetImageUri, featureType, config, handleProgress]);
|
|
100
|
+
|
|
101
|
+
const save = useCallback(async () => {
|
|
102
|
+
if (!state.processedVideoUrl) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await onSaveVideo(state.processedVideoUrl);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
setState((prev) => ({ ...prev, error: message }));
|
|
109
|
+
}
|
|
110
|
+
}, [state.processedVideoUrl, onSaveVideo]);
|
|
111
|
+
|
|
112
|
+
const reset = useCallback(() => {
|
|
113
|
+
setState(initialState);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...state,
|
|
118
|
+
selectSourceImage,
|
|
119
|
+
selectTargetImage,
|
|
120
|
+
process,
|
|
121
|
+
save,
|
|
122
|
+
reset,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress Calculator Utility
|
|
3
3
|
* Calculates progress based on generation stage
|
|
4
|
+
* Maps provider status to generation progress
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { GenerationStatus } from "../../domain/entities";
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
DEFAULT_PROGRESS_STAGES,
|
|
9
10
|
type ProgressStageConfig,
|
|
10
11
|
} from "../../domain/entities";
|
|
12
|
+
import type { AIJobStatusType } from "../../domain/interfaces/ai-provider.interface";
|
|
11
13
|
|
|
12
14
|
export interface ProgressOptions {
|
|
13
15
|
status: GenerationStatus;
|
|
@@ -95,3 +97,32 @@ export function createProgressTracker(stages?: ProgressStageConfig[]) {
|
|
|
95
97
|
},
|
|
96
98
|
};
|
|
97
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Maps provider job status to generation status
|
|
103
|
+
* Provider: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED
|
|
104
|
+
* Generation: idle, preparing, submitting, generating, polling, finalizing, completed, failed
|
|
105
|
+
*/
|
|
106
|
+
export function mapJobStatusToGenerationStatus(
|
|
107
|
+
jobStatus: AIJobStatusType
|
|
108
|
+
): GenerationStatus {
|
|
109
|
+
const statusMap: Record<AIJobStatusType, GenerationStatus> = {
|
|
110
|
+
IN_QUEUE: "submitting",
|
|
111
|
+
IN_PROGRESS: "generating",
|
|
112
|
+
COMPLETED: "completed",
|
|
113
|
+
FAILED: "failed",
|
|
114
|
+
};
|
|
115
|
+
return statusMap[jobStatus] ?? "generating";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Calculate progress from provider job status
|
|
120
|
+
* Uses default progress stages for consistent progress reporting
|
|
121
|
+
*/
|
|
122
|
+
export function getProgressFromJobStatus(
|
|
123
|
+
jobStatus: AIJobStatusType,
|
|
124
|
+
stages: ProgressStageConfig[] = DEFAULT_PROGRESS_STAGES
|
|
125
|
+
): number {
|
|
126
|
+
const generationStatus = mapJobStatusToGenerationStatus(jobStatus);
|
|
127
|
+
return getProgressForStatus({ status: generationStatus, stages });
|
|
128
|
+
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useStatusFilter Hook
|
|
3
|
-
* Handles status filtering (completed, pending, processing, failed)
|
|
4
|
-
* SOLID: Single Responsibility - Only handles status filter state
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useCallback, useMemo } from "react";
|
|
8
|
-
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
9
|
-
|
|
10
|
-
interface UseStatusFilterProps {
|
|
11
|
-
readonly options: FilterOption[];
|
|
12
|
-
readonly t: (key: string) => string;
|
|
13
|
-
readonly defaultId?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface UseStatusFilterReturn {
|
|
17
|
-
readonly selectedId: string;
|
|
18
|
-
readonly filterOptions: FilterOption[];
|
|
19
|
-
readonly hasActiveFilter: boolean;
|
|
20
|
-
readonly selectFilter: (id: string) => void;
|
|
21
|
-
readonly clearFilter: () => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function useStatusFilter({
|
|
25
|
-
options,
|
|
26
|
-
t,
|
|
27
|
-
defaultId = "all"
|
|
28
|
-
}: UseStatusFilterProps): UseStatusFilterReturn {
|
|
29
|
-
const [selectedId, setSelectedId] = useState(defaultId);
|
|
30
|
-
|
|
31
|
-
const filterOptions = useMemo(() =>
|
|
32
|
-
options.map(opt => ({
|
|
33
|
-
...opt,
|
|
34
|
-
label: opt.labelKey ? t(opt.labelKey) : opt.label
|
|
35
|
-
})),
|
|
36
|
-
[options, t]
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const selectFilter = useCallback((id: string) => {
|
|
40
|
-
setSelectedId(id);
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const clearFilter = useCallback(() => {
|
|
44
|
-
setSelectedId(defaultId);
|
|
45
|
-
}, [defaultId]);
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
selectedId,
|
|
49
|
-
filterOptions,
|
|
50
|
-
hasActiveFilter: selectedId !== defaultId,
|
|
51
|
-
selectFilter,
|
|
52
|
-
clearFilter
|
|
53
|
-
};
|
|
54
|
-
}
|