@umituz/react-native-design-system 4.23.97 → 4.23.100
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/atoms/button/AtomicButton.tsx +7 -0
- package/src/atoms/button/types/index.ts +4 -0
- package/src/atoms/input/hooks/useInputState.ts +3 -7
- package/src/haptics/infrastructure/services/HapticService.ts +1 -1
- package/src/media/infrastructure/hooks/useGenericMediaGeneration.ts +170 -0
- package/src/media/presentation/hooks/useCardMediaGeneration.ts +9 -113
- package/src/media/presentation/hooks/useCardMediaUpload.ts +5 -5
- package/src/media/presentation/hooks/useCardMediaValidation.ts +4 -1
- package/src/media/presentation/hooks/useMediaGeneration.ts +4 -87
- package/src/molecules/navigation/components/NavigationHeader.tsx +3 -3
- package/src/offline/index.ts +1 -0
- package/src/offline/infrastructure/storage/OfflineConfigStore.ts +34 -0
- package/src/offline/presentation/hooks/useOffline.ts +8 -4
- package/src/storage/domain/utils/devUtils.ts +0 -24
- package/src/storage/index.ts +1 -1
- package/src/storage/infrastructure/adapters/StorageService.ts +2 -7
- package/src/storage/infrastructure/repositories/BaseStorageOperations.ts +0 -3
- package/src/storage/presentation/hooks/CacheStorageOperations.ts +2 -8
- package/src/storage/presentation/hooks/useStore.ts +14 -5
- package/src/utilities/sharing/presentation/hooks/useSharing.ts +3 -3
- package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +0 -92
- package/src/media/domain/entities/CardMultimedia.types.ts +0 -120
- package/src/media/infrastructure/services/CardMediaGenerationService.README.md +0 -99
- package/src/media/infrastructure/services/CardMediaGenerationService.ts +0 -101
- package/src/media/infrastructure/services/CardMediaOptimizerService.README.md +0 -167
- package/src/media/infrastructure/services/CardMediaOptimizerService.ts +0 -36
- package/src/media/infrastructure/services/CardMediaUploadService.README.md +0 -123
- package/src/media/infrastructure/services/CardMediaUploadService.ts +0 -62
- package/src/media/infrastructure/services/CardMediaValidationService.README.md +0 -134
- package/src/media/infrastructure/services/CardMediaValidationService.ts +0 -81
- package/src/media/presentation/hooks/useCardMediaGeneration.README.md +0 -164
- package/src/media/presentation/hooks/useCardMediaUpload.README.md +0 -153
- package/src/media/presentation/hooks/useCardMediaValidation.README.md +0 -176
- package/src/storage/domain/utils/__tests__/devUtils.test.ts +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.23.
|
|
3
|
+
"version": "4.23.100",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -29,6 +29,9 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
|
|
|
29
29
|
textStyle,
|
|
30
30
|
activeOpacity = 0.8,
|
|
31
31
|
testID,
|
|
32
|
+
accessibilityLabel,
|
|
33
|
+
accessibilityHint,
|
|
34
|
+
accessibilityRole = 'button',
|
|
32
35
|
}) => {
|
|
33
36
|
const tokens = useAppDesignTokens();
|
|
34
37
|
|
|
@@ -80,6 +83,10 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
|
|
|
80
83
|
activeOpacity={activeOpacity}
|
|
81
84
|
disabled={isDisabled}
|
|
82
85
|
testID={testID}
|
|
86
|
+
accessibilityRole={accessibilityRole}
|
|
87
|
+
accessibilityLabel={accessibilityLabel || title || (typeof children === 'string' ? children : 'Button')}
|
|
88
|
+
accessibilityHint={accessibilityHint}
|
|
89
|
+
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
83
90
|
>
|
|
84
91
|
{loading ? (
|
|
85
92
|
<AtomicSpinner
|
|
@@ -24,6 +24,10 @@ export interface AtomicButtonProps {
|
|
|
24
24
|
readonly textStyle?: StyleProp<TextStyle>;
|
|
25
25
|
readonly activeOpacity?: number;
|
|
26
26
|
readonly testID?: string;
|
|
27
|
+
// Accessibility props
|
|
28
|
+
readonly accessibilityLabel?: string;
|
|
29
|
+
readonly accessibilityHint?: string;
|
|
30
|
+
readonly accessibilityRole?: 'button' | 'link';
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export interface ButtonSizeConfig {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
interface UseInputStateProps {
|
|
4
4
|
value?: string;
|
|
5
5
|
onChangeText?: (text: string) => void;
|
|
6
6
|
secureTextEntry?: boolean;
|
|
7
|
-
showPasswordToggle?: boolean;
|
|
8
7
|
maxLength?: number;
|
|
9
|
-
showCharacterCount?: boolean;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
interface UseInputStateReturn {
|
|
@@ -24,9 +22,7 @@ export const useInputState = ({
|
|
|
24
22
|
value = '',
|
|
25
23
|
onChangeText,
|
|
26
24
|
secureTextEntry = false,
|
|
27
|
-
showPasswordToggle: _showPasswordToggle = false,
|
|
28
25
|
maxLength,
|
|
29
|
-
showCharacterCount: _showCharacterCount = false,
|
|
30
26
|
}: UseInputStateProps = {}): UseInputStateReturn => {
|
|
31
27
|
const [localValue, setLocalValue] = useState(value);
|
|
32
28
|
const [isFocused, setIsFocused] = useState(false);
|
|
@@ -49,7 +45,7 @@ export const useInputState = ({
|
|
|
49
45
|
const characterCount = localValue.length;
|
|
50
46
|
const isAtMaxLength = maxLength ? characterCount >= maxLength : false;
|
|
51
47
|
|
|
52
|
-
return {
|
|
48
|
+
return useMemo(() => ({
|
|
53
49
|
localValue,
|
|
54
50
|
isFocused,
|
|
55
51
|
isPasswordVisible,
|
|
@@ -58,5 +54,5 @@ export const useInputState = ({
|
|
|
58
54
|
setIsFocused,
|
|
59
55
|
handleTextChange,
|
|
60
56
|
togglePasswordVisibility,
|
|
61
|
-
};
|
|
57
|
+
}), [localValue, isFocused, isPasswordVisible, characterCount, isAtMaxLength, handleTextChange, togglePasswordVisibility]);
|
|
62
58
|
};
|
|
@@ -15,7 +15,7 @@ import type { ImpactStyle, NotificationType, HapticPattern } from '../../domain/
|
|
|
15
15
|
* Log error in development mode only
|
|
16
16
|
*/
|
|
17
17
|
function logError(method: string, error: unknown): void {
|
|
18
|
-
if (
|
|
18
|
+
if (__DEV__) {
|
|
19
19
|
console.error(`[DesignSystem] HapticService.${method} error:`, error);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Media Generation Hook
|
|
3
|
+
* Shared implementation for both Media and CardMedia generation
|
|
4
|
+
* Eliminates ~600 LOC duplication
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
|
|
9
|
+
interface GenericMediaAttachment {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
position: string;
|
|
13
|
+
url: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
fileSize: number;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
duration?: number;
|
|
18
|
+
thumbnailUrl?: string;
|
|
19
|
+
caption?: string;
|
|
20
|
+
isDownloaded: boolean;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface GenericMediaGenerationRequest {
|
|
25
|
+
type: "text_to_image" | "text_to_audio" | "image_search";
|
|
26
|
+
input: {
|
|
27
|
+
text?: string;
|
|
28
|
+
prompt?: string;
|
|
29
|
+
language?: string;
|
|
30
|
+
voice?: "male" | "female" | "neutral";
|
|
31
|
+
style?: "realistic" | "cartoon" | "artistic";
|
|
32
|
+
};
|
|
33
|
+
options: {
|
|
34
|
+
maxResults?: number;
|
|
35
|
+
quality?: "low" | "medium" | "high";
|
|
36
|
+
format?: "jpeg" | "png" | "mp3" | "wav";
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface GenericMediaGenerationResult<TAttachment> {
|
|
41
|
+
success: boolean;
|
|
42
|
+
attachments: TAttachment[];
|
|
43
|
+
creditsUsed: number;
|
|
44
|
+
processingTime: number;
|
|
45
|
+
error?: string;
|
|
46
|
+
requestId: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UseGenericMediaGenerationResult<TAttachment, TRequest> {
|
|
50
|
+
generateMedia: (request: TRequest) => Promise<GenericMediaGenerationResult<TAttachment>>;
|
|
51
|
+
isGenerating: boolean;
|
|
52
|
+
generationResult: GenericMediaGenerationResult<TAttachment> | null;
|
|
53
|
+
error: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generic implementation of media generation logic
|
|
58
|
+
* Type-safe through attachment factory pattern
|
|
59
|
+
*/
|
|
60
|
+
export function useGenericMediaGeneration<
|
|
61
|
+
TAttachment extends GenericMediaAttachment,
|
|
62
|
+
TRequest extends GenericMediaGenerationRequest
|
|
63
|
+
>(
|
|
64
|
+
attachmentFactory: (baseAttachment: GenericMediaAttachment) => TAttachment
|
|
65
|
+
): UseGenericMediaGenerationResult<TAttachment, TRequest> {
|
|
66
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
67
|
+
const [generationResult, setGenerationResult] =
|
|
68
|
+
useState<GenericMediaGenerationResult<TAttachment> | null>(null);
|
|
69
|
+
const [error, setError] = useState<string | null>(null);
|
|
70
|
+
|
|
71
|
+
const generateMedia = useCallback(
|
|
72
|
+
async (request: TRequest): Promise<GenericMediaGenerationResult<TAttachment>> => {
|
|
73
|
+
try {
|
|
74
|
+
setIsGenerating(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
|
|
77
|
+
// Simulate generation
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
79
|
+
|
|
80
|
+
const baseAttachments: GenericMediaAttachment[] = [];
|
|
81
|
+
|
|
82
|
+
switch (request.type) {
|
|
83
|
+
case "text_to_image":
|
|
84
|
+
for (let i = 0; i < (request.options.maxResults || 1); i++) {
|
|
85
|
+
baseAttachments.push({
|
|
86
|
+
id: `ai_img_${Date.now()}_${i}`,
|
|
87
|
+
type: "image",
|
|
88
|
+
position: "both",
|
|
89
|
+
url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
|
|
90
|
+
filename: `ai_generated_${i}.jpg`,
|
|
91
|
+
fileSize: 150000,
|
|
92
|
+
mimeType: "image/jpeg",
|
|
93
|
+
isDownloaded: false,
|
|
94
|
+
createdAt: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case "text_to_audio":
|
|
100
|
+
baseAttachments.push({
|
|
101
|
+
id: `ai_audio_${Date.now()}`,
|
|
102
|
+
type: "audio",
|
|
103
|
+
position: "back",
|
|
104
|
+
url: `https://example.com/audio_${Date.now()}.mp3`,
|
|
105
|
+
filename: `ai_generated_${Date.now()}.mp3`,
|
|
106
|
+
fileSize: 80000,
|
|
107
|
+
mimeType: "audio/mp3",
|
|
108
|
+
duration: 10,
|
|
109
|
+
isDownloaded: false,
|
|
110
|
+
createdAt: new Date().toISOString(),
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case "image_search":
|
|
115
|
+
for (let i = 0; i < (request.options.maxResults || 5); i++) {
|
|
116
|
+
baseAttachments.push({
|
|
117
|
+
id: `search_img_${Date.now()}_${i}`,
|
|
118
|
+
type: "image",
|
|
119
|
+
position: "both",
|
|
120
|
+
url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
|
|
121
|
+
filename: `search_result_${i}.jpg`,
|
|
122
|
+
fileSize: 120000,
|
|
123
|
+
mimeType: "image/jpeg",
|
|
124
|
+
isDownloaded: false,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const attachments = baseAttachments.map(attachmentFactory);
|
|
132
|
+
|
|
133
|
+
const result: GenericMediaGenerationResult<TAttachment> = {
|
|
134
|
+
success: true,
|
|
135
|
+
attachments,
|
|
136
|
+
creditsUsed: request.type === "text_to_image" ? 5 : request.type === "text_to_audio" ? 3 : 2,
|
|
137
|
+
processingTime: 3000,
|
|
138
|
+
requestId: `req_${Date.now()}`,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
setGenerationResult(result);
|
|
142
|
+
return result;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const errorMessage =
|
|
145
|
+
err instanceof Error ? err.message : "Generation failed";
|
|
146
|
+
setError(errorMessage);
|
|
147
|
+
setIsGenerating(false);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
attachments: [],
|
|
152
|
+
creditsUsed: 0,
|
|
153
|
+
processingTime: 0,
|
|
154
|
+
error: errorMessage,
|
|
155
|
+
requestId: "",
|
|
156
|
+
};
|
|
157
|
+
} finally {
|
|
158
|
+
setIsGenerating(false);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
[attachmentFactory],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
generateMedia,
|
|
166
|
+
isGenerating,
|
|
167
|
+
generationResult,
|
|
168
|
+
error,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -1,124 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Card Media Generation Hook
|
|
3
3
|
* Hook for generating card media with AI
|
|
4
|
+
* Now a thin wrapper around useGenericMediaGeneration
|
|
5
|
+
*
|
|
6
|
+
* Note: CardMedia types are aliases of Media types for backward compatibility
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import {
|
|
9
|
+
import { useGenericMediaGeneration } from "../../infrastructure/hooks/useGenericMediaGeneration";
|
|
7
10
|
import type { UseCardMediaGenerationResult } from "./card-multimedia.types";
|
|
8
11
|
import type {
|
|
9
|
-
CardMediaAttachment,
|
|
10
|
-
CardMediaGenerationRequest,
|
|
11
|
-
|
|
12
|
-
} from "../../domain/entities/CardMultimedia.types";
|
|
12
|
+
MediaAttachment as CardMediaAttachment,
|
|
13
|
+
MediaGenerationRequest as CardMediaGenerationRequest,
|
|
14
|
+
} from "../../domain/entities/MultimediaFlashcardTypes";
|
|
13
15
|
|
|
14
16
|
export const useCardMediaGeneration = (): UseCardMediaGenerationResult => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
useState<CardMediaGenerationResult | null>(null);
|
|
18
|
-
const [error, setError] = useState<string | null>(null);
|
|
19
|
-
|
|
20
|
-
const generateMedia = useCallback(
|
|
21
|
-
async (
|
|
22
|
-
request: CardMediaGenerationRequest,
|
|
23
|
-
): Promise<CardMediaGenerationResult> => {
|
|
24
|
-
try {
|
|
25
|
-
setIsGenerating(true);
|
|
26
|
-
setError(null);
|
|
27
|
-
|
|
28
|
-
// Simulate generation
|
|
29
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
30
|
-
|
|
31
|
-
const attachments: CardMediaAttachment[] = [];
|
|
32
|
-
|
|
33
|
-
switch (request.type) {
|
|
34
|
-
case "text_to_image":
|
|
35
|
-
for (let i = 0; i < (request.options.maxResults || 1); i++) {
|
|
36
|
-
attachments.push({
|
|
37
|
-
id: `ai_img_${Date.now()}_${i}`,
|
|
38
|
-
type: "image",
|
|
39
|
-
position: "both",
|
|
40
|
-
url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
|
|
41
|
-
filename: `ai_generated_${i}.jpg`,
|
|
42
|
-
fileSize: 150000, // 150KB
|
|
43
|
-
mimeType: "image/jpeg",
|
|
44
|
-
isDownloaded: false,
|
|
45
|
-
createdAt: new Date().toISOString(),
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
break;
|
|
49
|
-
|
|
50
|
-
case "text_to_audio":
|
|
51
|
-
attachments.push({
|
|
52
|
-
id: `ai_audio_${Date.now()}`,
|
|
53
|
-
type: "audio",
|
|
54
|
-
position: "back",
|
|
55
|
-
url: `https://example.com/audio_${Date.now()}.mp3`,
|
|
56
|
-
filename: `ai_generated_${Date.now()}.mp3`,
|
|
57
|
-
fileSize: 80000, // 80KB
|
|
58
|
-
mimeType: "audio/mp3",
|
|
59
|
-
duration: 10, // 10 seconds
|
|
60
|
-
isDownloaded: false,
|
|
61
|
-
createdAt: new Date().toISOString(),
|
|
62
|
-
});
|
|
63
|
-
break;
|
|
64
|
-
|
|
65
|
-
case "image_search":
|
|
66
|
-
for (let i = 0; i < (request.options.maxResults || 5); i++) {
|
|
67
|
-
attachments.push({
|
|
68
|
-
id: `search_img_${Date.now()}_${i}`,
|
|
69
|
-
type: "image",
|
|
70
|
-
position: "both",
|
|
71
|
-
url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
|
|
72
|
-
filename: `search_result_${i}.jpg`,
|
|
73
|
-
fileSize: 120000, // 120KB
|
|
74
|
-
mimeType: "image/jpeg",
|
|
75
|
-
isDownloaded: false,
|
|
76
|
-
createdAt: new Date().toISOString(),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const result: CardMediaGenerationResult = {
|
|
83
|
-
success: true,
|
|
84
|
-
attachments,
|
|
85
|
-
creditsUsed:
|
|
86
|
-
request.type === "text_to_image"
|
|
87
|
-
? 5
|
|
88
|
-
: request.type === "text_to_audio"
|
|
89
|
-
? 3
|
|
90
|
-
: 2,
|
|
91
|
-
processingTime: 3000,
|
|
92
|
-
requestId: `req_${Date.now()}`,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
setGenerationResult(result);
|
|
96
|
-
return result;
|
|
97
|
-
} catch (err) {
|
|
98
|
-
const errorMessage =
|
|
99
|
-
err instanceof Error ? err.message : "Generation failed";
|
|
100
|
-
setError(errorMessage);
|
|
101
|
-
setIsGenerating(false);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
success: false,
|
|
105
|
-
attachments: [],
|
|
106
|
-
creditsUsed: 0,
|
|
107
|
-
processingTime: 0,
|
|
108
|
-
error: errorMessage,
|
|
109
|
-
requestId: "",
|
|
110
|
-
};
|
|
111
|
-
} finally {
|
|
112
|
-
setIsGenerating(false);
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
[],
|
|
17
|
+
return useGenericMediaGeneration<CardMediaAttachment, CardMediaGenerationRequest>(
|
|
18
|
+
(baseAttachment) => baseAttachment as CardMediaAttachment
|
|
116
19
|
);
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
generateMedia,
|
|
120
|
-
isGenerating,
|
|
121
|
-
generationResult,
|
|
122
|
-
error,
|
|
123
|
-
};
|
|
124
20
|
};
|
|
@@ -8,11 +8,11 @@ import { generateThumbnail, getMediaDuration } from "../../infrastructure/utils/
|
|
|
8
8
|
import { getMediaTypeFromMime } from "../../infrastructure/utils/mime-type-detector";
|
|
9
9
|
import type { UseCardMediaUploadResult } from "./card-multimedia.types";
|
|
10
10
|
import type {
|
|
11
|
-
CardMediaAttachment,
|
|
12
|
-
CardMediaCompressionOptions,
|
|
13
|
-
CardMediaFile,
|
|
14
|
-
CardMediaUploadProgress,
|
|
15
|
-
} from "../../domain/entities/
|
|
11
|
+
MediaAttachment as CardMediaAttachment,
|
|
12
|
+
MediaCompressionOptions as CardMediaCompressionOptions,
|
|
13
|
+
MediaFile as CardMediaFile,
|
|
14
|
+
MediaUploadProgress as CardMediaUploadProgress,
|
|
15
|
+
} from "../../domain/entities/MultimediaFlashcardTypes";
|
|
16
16
|
|
|
17
17
|
export const useCardMediaUpload = (): UseCardMediaUploadResult => {
|
|
18
18
|
const [isUploading, setIsUploading] = useState(false);
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import { useState, useCallback } from "react";
|
|
7
7
|
import { formatFileSize } from "../../infrastructure/utils/media-collection-utils";
|
|
8
8
|
import type { UseCardMediaValidationResult } from "./card-multimedia.types";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
MediaValidation as CardMediaValidation,
|
|
11
|
+
MediaFile as CardMediaFile,
|
|
12
|
+
} from "../../domain/entities/MultimediaFlashcardTypes";
|
|
10
13
|
|
|
11
14
|
export const useCardMediaValidation = (): UseCardMediaValidationResult => {
|
|
12
15
|
const [isValidating, setIsValidating] = useState(false);
|
|
@@ -1,101 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Media Generation Hook
|
|
3
3
|
* Hook for generating media with AI
|
|
4
|
+
* Now a thin wrapper around useGenericMediaGeneration
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { useGenericMediaGeneration } from "../../infrastructure/hooks/useGenericMediaGeneration";
|
|
7
8
|
import type { UseMediaGenerationResult } from "./multimedia.types";
|
|
8
9
|
import type {
|
|
9
10
|
MediaAttachment,
|
|
10
11
|
MediaGenerationRequest,
|
|
11
|
-
MediaGenerationResult,
|
|
12
12
|
} from "../../domain/entities/MultimediaFlashcardTypes";
|
|
13
13
|
|
|
14
14
|
export const useMediaGeneration = (): UseMediaGenerationResult => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
useState<MediaGenerationResult | null>(null);
|
|
18
|
-
const [error, setError] = useState<string | null>(null);
|
|
19
|
-
|
|
20
|
-
const generateMedia = useCallback(
|
|
21
|
-
async (request: MediaGenerationRequest): Promise<MediaGenerationResult> => {
|
|
22
|
-
try {
|
|
23
|
-
setIsGenerating(true);
|
|
24
|
-
setError(null);
|
|
25
|
-
|
|
26
|
-
// Simulate generation
|
|
27
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
28
|
-
|
|
29
|
-
const attachments: MediaAttachment[] = [];
|
|
30
|
-
|
|
31
|
-
switch (request.type) {
|
|
32
|
-
case "text_to_image":
|
|
33
|
-
for (let i = 0; i < (request.options.maxResults || 1); i++) {
|
|
34
|
-
attachments.push({
|
|
35
|
-
id: `ai_img_${Date.now()}_${i}`,
|
|
36
|
-
type: "image",
|
|
37
|
-
position: "both",
|
|
38
|
-
url: `https://picsum.photos/400/300?random=${Date.now() + i}`,
|
|
39
|
-
filename: `ai_generated_${i}.jpg`,
|
|
40
|
-
fileSize: 150000, // 150KB
|
|
41
|
-
mimeType: "image/jpeg",
|
|
42
|
-
isDownloaded: false,
|
|
43
|
-
createdAt: new Date().toISOString(),
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
break;
|
|
47
|
-
|
|
48
|
-
case "text_to_audio":
|
|
49
|
-
attachments.push({
|
|
50
|
-
id: `ai_audio_${Date.now()}`,
|
|
51
|
-
type: "audio",
|
|
52
|
-
position: "back",
|
|
53
|
-
url: `https://example.com/audio_${Date.now()}.mp3`,
|
|
54
|
-
filename: `ai_generated_${Date.now()}.mp3`,
|
|
55
|
-
fileSize: 80000, // 80KB
|
|
56
|
-
mimeType: "audio/mp3",
|
|
57
|
-
duration: 10, // 10 seconds
|
|
58
|
-
isDownloaded: false,
|
|
59
|
-
createdAt: new Date().toISOString(),
|
|
60
|
-
});
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const result: MediaGenerationResult = {
|
|
65
|
-
success: true,
|
|
66
|
-
attachments,
|
|
67
|
-
creditsUsed: request.type === "text_to_image" ? 5 : 3,
|
|
68
|
-
processingTime: 3000,
|
|
69
|
-
requestId: `req_${Date.now()}`,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
setGenerationResult(result);
|
|
73
|
-
return result;
|
|
74
|
-
} catch (err) {
|
|
75
|
-
const errorMessage =
|
|
76
|
-
err instanceof Error ? err.message : "Generation failed";
|
|
77
|
-
setError(errorMessage);
|
|
78
|
-
setIsGenerating(false);
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
success: false,
|
|
82
|
-
attachments: [],
|
|
83
|
-
creditsUsed: 0,
|
|
84
|
-
processingTime: 0,
|
|
85
|
-
error: errorMessage,
|
|
86
|
-
requestId: "",
|
|
87
|
-
};
|
|
88
|
-
} finally {
|
|
89
|
-
setIsGenerating(false);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
[],
|
|
15
|
+
return useGenericMediaGeneration<MediaAttachment, MediaGenerationRequest>(
|
|
16
|
+
(baseAttachment) => baseAttachment as MediaAttachment
|
|
93
17
|
);
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
generateMedia,
|
|
97
|
-
isGenerating,
|
|
98
|
-
generationResult,
|
|
99
|
-
error,
|
|
100
|
-
};
|
|
101
18
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
3
|
import { AtomicText } from '../../../atoms';
|
|
4
4
|
import { AtomicIcon, useIconName } from '../../../atoms';
|
|
@@ -20,7 +20,7 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
|
|
|
20
20
|
const insets = useSafeAreaInsets();
|
|
21
21
|
const arrowLeftIcon = useIconName('arrowLeft');
|
|
22
22
|
|
|
23
|
-
const styles = StyleSheet.create({
|
|
23
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
24
24
|
container: {
|
|
25
25
|
paddingTop: insets.top,
|
|
26
26
|
paddingHorizontal: tokens.spacing.md,
|
|
@@ -45,7 +45,7 @@ export const NavigationHeader: React.FC<NavigationHeaderProps> = ({
|
|
|
45
45
|
flex: 1,
|
|
46
46
|
textAlign: 'left',
|
|
47
47
|
},
|
|
48
|
-
});
|
|
48
|
+
}), [tokens, insets]);
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
51
|
<View style={styles.container}>
|
package/src/offline/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type {
|
|
|
14
14
|
|
|
15
15
|
// Store
|
|
16
16
|
export { useOfflineStore } from './infrastructure/storage/OfflineStore';
|
|
17
|
+
export { useOfflineConfigStore } from './infrastructure/storage/OfflineConfigStore';
|
|
17
18
|
|
|
18
19
|
// Hooks
|
|
19
20
|
export { useOffline, configureOffline } from './presentation/hooks/useOffline';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline Config Store
|
|
3
|
+
* Centralized configuration for offline functionality
|
|
4
|
+
* Replaces module-level mutable state with Zustand store
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { create } from 'zustand';
|
|
8
|
+
import type { OfflineConfig } from '../../types';
|
|
9
|
+
|
|
10
|
+
interface OfflineConfigStore {
|
|
11
|
+
config: OfflineConfig;
|
|
12
|
+
setConfig: (config: OfflineConfig) => void;
|
|
13
|
+
mergeConfig: (partialConfig: OfflineConfig) => void;
|
|
14
|
+
reset: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useOfflineConfigStore = create<OfflineConfigStore>((set) => ({
|
|
18
|
+
config: {},
|
|
19
|
+
|
|
20
|
+
setConfig: (config) => set({ config }),
|
|
21
|
+
|
|
22
|
+
mergeConfig: (partialConfig) => set((state) => ({
|
|
23
|
+
config: { ...state.config, ...partialConfig }
|
|
24
|
+
})),
|
|
25
|
+
|
|
26
|
+
reset: () => set({ config: {} }),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get current config (for non-React contexts)
|
|
31
|
+
*/
|
|
32
|
+
export const getOfflineConfig = (): OfflineConfig => {
|
|
33
|
+
return useOfflineConfigStore.getState().config;
|
|
34
|
+
};
|
|
@@ -10,6 +10,7 @@ import type { NetworkState as ExpoNetworkState } from 'expo-network';
|
|
|
10
10
|
import type { NetworkState, OfflineConfig } from '../../types';
|
|
11
11
|
import { useOfflineStore } from '../../infrastructure/storage/OfflineStore';
|
|
12
12
|
import { networkEvents } from '../../infrastructure/events/NetworkEvents';
|
|
13
|
+
import { useOfflineConfigStore } from '../../infrastructure/storage/OfflineConfigStore';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Convert expo-network state to our internal format
|
|
@@ -21,14 +22,17 @@ const toNetworkState = (state: ExpoNetworkState): NetworkState => ({
|
|
|
21
22
|
details: null,
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Configure offline settings globally
|
|
27
|
+
* This is a facade over the config store for backward compatibility
|
|
28
|
+
*/
|
|
26
29
|
export const configureOffline = (config: OfflineConfig): void => {
|
|
27
|
-
|
|
30
|
+
useOfflineConfigStore.getState().setConfig(config);
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
export const useOffline = (config?: OfflineConfig) => {
|
|
31
34
|
const store = useOfflineStore();
|
|
35
|
+
const globalConfig = useOfflineConfigStore((state) => state.config);
|
|
32
36
|
const isInitialized = useRef(false);
|
|
33
37
|
const previousStateRef = useRef<NetworkState | null>(null);
|
|
34
38
|
const isMountedRef = useRef(true);
|
|
@@ -36,7 +40,7 @@ export const useOffline = (config?: OfflineConfig) => {
|
|
|
36
40
|
// Memoize merged config to prevent unnecessary effect re-runs
|
|
37
41
|
const mergedConfig = useMemo(
|
|
38
42
|
() => ({ ...globalConfig, ...config }),
|
|
39
|
-
[config]
|
|
43
|
+
[globalConfig, config]
|
|
40
44
|
);
|
|
41
45
|
|
|
42
46
|
const handleNetworkStateChange = useCallback((state: ExpoNetworkState) => {
|
|
@@ -9,27 +9,3 @@ export const isDev = (): boolean => {
|
|
|
9
9
|
return __DEV__;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Log warning in development mode only
|
|
14
|
-
* All logs are disabled
|
|
15
|
-
*/
|
|
16
|
-
export const devWarn = (_message: string, ..._args: unknown[]): void => {
|
|
17
|
-
// Disabled
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Log error in development mode only
|
|
22
|
-
* All logs are disabled
|
|
23
|
-
*/
|
|
24
|
-
export const devError = (_message: string, ..._args: unknown[]): void => {
|
|
25
|
-
// Disabled
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Log info in development mode only
|
|
30
|
-
* All logs are disabled
|
|
31
|
-
*/
|
|
32
|
-
export const devLog = (_message: string, ..._args: unknown[]): void => {
|
|
33
|
-
// Disabled
|
|
34
|
-
};
|
|
35
|
-
|
package/src/storage/index.ts
CHANGED
|
@@ -82,7 +82,7 @@ export { TIME_MS, DEFAULT_TTL, CACHE_VERSION } from './domain/constants/CacheDef
|
|
|
82
82
|
// DOMAIN LAYER - Development Utilities
|
|
83
83
|
// =============================================================================
|
|
84
84
|
|
|
85
|
-
export { isDev
|
|
85
|
+
export { isDev } from './domain/utils/devUtils';
|
|
86
86
|
|
|
87
87
|
// =============================================================================
|
|
88
88
|
// DOMAIN LAYER - Store Types
|