@umituz/react-native-design-system 4.27.14 → 4.27.16
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 +11 -6
- package/src/atoms/GlassView/GlassView.tsx +0 -2
- package/src/core/cache/domain/CleanupStrategy.ts +115 -0
- package/src/core/cache/domain/UnifiedCache.ts +196 -0
- package/src/core/cache/domain/types.ts +18 -0
- package/src/core/cache/index.ts +17 -0
- package/src/core/cache/infrastructure/CacheFactory.ts +102 -0
- package/src/core/index.ts +23 -0
- package/src/core/permissions/domain/PermissionHandler.ts +139 -0
- package/src/core/permissions/domain/types.ts +49 -0
- package/src/core/permissions/index.ts +15 -0
- package/src/core/repositories/domain/RepositoryKeyFactory.ts +41 -0
- package/src/core/repositories/domain/RepositoryUtils.ts +86 -0
- package/src/core/repositories/domain/types.ts +59 -0
- package/src/core/repositories/index.ts +23 -0
- package/src/device/detection/deviceDetection.ts +8 -2
- package/src/haptics/infrastructure/services/HapticService.ts +4 -1
- package/src/hooks/index.ts +22 -25
- package/src/index.ts +1 -0
- package/src/infinite-scroll/presentation/hooks/useInfiniteScroll.ts +32 -29
- package/src/media/domain/strategies/CameraPickerStrategy.ts +61 -0
- package/src/media/domain/strategies/LibraryPickerStrategy.ts +54 -0
- package/src/media/domain/strategies/PickerStrategy.ts +61 -0
- package/src/media/domain/strategies/index.ts +13 -0
- package/src/media/infrastructure/services/MediaPickerService.ts +109 -110
- package/src/media/infrastructure/utils/PermissionManager.ts +68 -66
- package/src/media/infrastructure/utils/mediaPickerMappers.ts +29 -6
- package/src/media/presentation/hooks/useMedia.ts +16 -4
- package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +0 -1
- package/src/offline/index.ts +1 -1
- package/src/offline/presentation/hooks/useOffline.ts +0 -8
- package/src/storage/README.md +0 -1
- package/src/storage/cache/domain/types/README.md +0 -1
- package/src/storage/cache/infrastructure/TTLCache.ts +3 -0
- package/src/storage/cache/presentation/README.md +0 -1
- package/src/storage/cache/presentation/useCachedValue.ts +12 -2
- package/src/storage/domain/constants/README.md +0 -1
- package/src/storage/infrastructure/adapters/README.md +0 -1
- package/src/storage/presentation/hooks/README.md +0 -6
- package/src/storage/presentation/hooks/useStorageState.ts +13 -4
- package/src/tanstack/domain/repositories/BaseRepository.ts +18 -1
- package/src/theme/hooks/useAppDesignTokens.ts +29 -3
- package/src/timezone/infrastructure/services/TimezoneProvider.ts +2 -2
- package/src/uuid/infrastructure/utils/UUIDUtils.ts +4 -1
- package/src/init/index.ts +0 -29
- package/src/layouts/ScreenLayout/index.ts +0 -1
- package/src/layouts/index.ts +0 -5
- package/src/molecules/SearchBar/index.ts +0 -4
- package/src/molecules/StepProgress/index.ts +0 -1
- package/src/molecules/alerts/index.ts +0 -47
- package/src/molecules/bottom-sheet/index.ts +0 -10
- package/src/molecules/circular-menu/index.ts +0 -3
- package/src/molecules/filter-group/index.ts +0 -3
- package/src/molecules/index.ts +0 -38
- package/src/molecules/info-grid/index.ts +0 -3
- package/src/molecules/swipe-actions/index.ts +0 -6
- package/src/presentation/utils/variants/index.ts +0 -6
- package/src/timezone/infrastructure/utils/SimpleCache.ts +0 -109
- package/src/utilities/index.ts +0 -6
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Permissions Domain Types
|
|
3
|
+
*
|
|
4
|
+
* Generic types for permission handling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PermissionMethod = 'request' | 'get';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Permission status types
|
|
11
|
+
* Apps can extend this for custom permission types
|
|
12
|
+
*/
|
|
13
|
+
export type PermissionStatus = 'granted' | 'denied' | 'limited' | 'undetermined';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Permission result with metadata
|
|
17
|
+
*/
|
|
18
|
+
export interface PermissionResult {
|
|
19
|
+
status: PermissionStatus;
|
|
20
|
+
canAskAgain?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Permission handler configuration
|
|
25
|
+
*/
|
|
26
|
+
export interface PermissionHandlerConfig<TPermission extends PermissionStatus = PermissionStatus> {
|
|
27
|
+
/**
|
|
28
|
+
* Map permission API methods
|
|
29
|
+
*/
|
|
30
|
+
methods: PermissionMethods;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Map API status to domain permission type
|
|
34
|
+
*/
|
|
35
|
+
statusMapper: (apiStatus: string) => TPermission;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default status when permission fails
|
|
39
|
+
*/
|
|
40
|
+
defaultStatus?: TPermission;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Permission API methods mapping
|
|
45
|
+
*/
|
|
46
|
+
export interface PermissionMethods {
|
|
47
|
+
request: Record<string, () => Promise<PermissionResult>>;
|
|
48
|
+
get: Record<string, () => Promise<PermissionResult>>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Permissions Module
|
|
3
|
+
*
|
|
4
|
+
* Generic permission handling with configurable methods and status mapping.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { PermissionHandler } from './domain/PermissionHandler';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
PermissionMethod,
|
|
11
|
+
PermissionStatus,
|
|
12
|
+
PermissionResult,
|
|
13
|
+
PermissionHandlerConfig,
|
|
14
|
+
PermissionMethods,
|
|
15
|
+
} from './domain/types';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository Key Factory
|
|
3
|
+
*
|
|
4
|
+
* Generic query key factory for repositories.
|
|
5
|
+
* Provides consistent query key structure across all repositories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { QueryKeyFactory, ListParams } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create query key factory for a resource
|
|
12
|
+
*
|
|
13
|
+
* @param resource - Resource name (e.g., 'users', 'posts')
|
|
14
|
+
* @returns Query key factory
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const keys = createRepositoryKeyFactory('users');
|
|
19
|
+
*
|
|
20
|
+
* keys.all(); // ['users']
|
|
21
|
+
* keys.lists(); // ['users', 'list']
|
|
22
|
+
* keys.list({ page: 1 }); // ['users', 'list', { page: 1 }]
|
|
23
|
+
* keys.details(); // ['users', 'detail']
|
|
24
|
+
* keys.detail(123); // ['users', 'detail', 123]
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function createRepositoryKeyFactory(
|
|
28
|
+
resource: string
|
|
29
|
+
): QueryKeyFactory {
|
|
30
|
+
return {
|
|
31
|
+
all: () => [resource] as const,
|
|
32
|
+
|
|
33
|
+
lists: () => [resource, 'list'] as const,
|
|
34
|
+
|
|
35
|
+
list: (params: ListParams) => [resource, 'list', params] as const,
|
|
36
|
+
|
|
37
|
+
details: () => [resource, 'detail'] as const,
|
|
38
|
+
|
|
39
|
+
detail: (id: string | number) => [resource, 'detail', id] as const,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common utilities for repository implementations.
|
|
5
|
+
* Provides caching options and helper functions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { RepositoryOptions, ListParams } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default cache options
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CACHE_OPTIONS = {
|
|
14
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
15
|
+
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Merge repository options with defaults
|
|
20
|
+
*
|
|
21
|
+
* @param options - User provided options
|
|
22
|
+
* @returns Merged options
|
|
23
|
+
*/
|
|
24
|
+
export function mergeRepositoryOptions(
|
|
25
|
+
options: RepositoryOptions = {}
|
|
26
|
+
): Required<RepositoryOptions> {
|
|
27
|
+
return {
|
|
28
|
+
cache: {
|
|
29
|
+
staleTime: options.cache?.staleTime ?? DEFAULT_CACHE_OPTIONS.staleTime,
|
|
30
|
+
gcTime: options.cache?.gcTime ?? DEFAULT_CACHE_OPTIONS.gcTime,
|
|
31
|
+
},
|
|
32
|
+
debug: options.debug ?? __DEV__,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get cache options from repository options
|
|
38
|
+
*
|
|
39
|
+
* @param options - Repository options
|
|
40
|
+
* @returns Cache options with defaults
|
|
41
|
+
*/
|
|
42
|
+
export function getCacheOptions(options: RepositoryOptions): {
|
|
43
|
+
staleTime: number;
|
|
44
|
+
gcTime: number;
|
|
45
|
+
} {
|
|
46
|
+
const cache = options.cache ?? {};
|
|
47
|
+
return {
|
|
48
|
+
staleTime: cache.staleTime ?? DEFAULT_CACHE_OPTIONS.staleTime,
|
|
49
|
+
gcTime: cache.gcTime ?? DEFAULT_CACHE_OPTIONS.gcTime,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Normalize list parameters
|
|
55
|
+
*
|
|
56
|
+
* @param params - List parameters
|
|
57
|
+
* @returns Normalized parameters
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeListParams(params: ListParams = {}): ListParams {
|
|
60
|
+
return {
|
|
61
|
+
page: params.page ?? 1,
|
|
62
|
+
limit: params.limit ?? 20,
|
|
63
|
+
sort: params.sort ?? 'createdAt',
|
|
64
|
+
filter: params.filter ?? {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create debug logger for repository
|
|
70
|
+
*
|
|
71
|
+
* @param resource - Resource name
|
|
72
|
+
* @param enabled - Enable logging
|
|
73
|
+
* @returns Logger function
|
|
74
|
+
*/
|
|
75
|
+
export function createRepositoryLogger(
|
|
76
|
+
resource: string,
|
|
77
|
+
enabled: boolean = __DEV__
|
|
78
|
+
): (method: string, ...args: unknown[]) => void {
|
|
79
|
+
if (!enabled) {
|
|
80
|
+
return () => {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (method: string, ...args: unknown[]) => {
|
|
84
|
+
console.log(`[Repository:${resource}] ${method}`, ...args);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Repository Domain Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for repository implementations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Repository options
|
|
9
|
+
*/
|
|
10
|
+
export interface RepositoryOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Cache configuration
|
|
13
|
+
*/
|
|
14
|
+
cache?: {
|
|
15
|
+
staleTime?: number;
|
|
16
|
+
gcTime?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enable debug logging
|
|
21
|
+
*/
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* List parameters for fetch operations
|
|
27
|
+
*/
|
|
28
|
+
export interface ListParams {
|
|
29
|
+
page?: number;
|
|
30
|
+
limit?: number;
|
|
31
|
+
sort?: string;
|
|
32
|
+
filter?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create parameters
|
|
37
|
+
*/
|
|
38
|
+
export interface CreateParams<TVariables> {
|
|
39
|
+
data: TVariables;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update parameters
|
|
44
|
+
*/
|
|
45
|
+
export interface UpdateParams<TVariables> {
|
|
46
|
+
id: string | number;
|
|
47
|
+
data: TVariables;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Query key factory result
|
|
52
|
+
*/
|
|
53
|
+
export type QueryKeyFactory = {
|
|
54
|
+
all: () => readonly string[];
|
|
55
|
+
lists: () => readonly string[];
|
|
56
|
+
list: (params: ListParams) => readonly unknown[];
|
|
57
|
+
details: () => readonly string[];
|
|
58
|
+
detail: (id: string | number) => readonly unknown[];
|
|
59
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Repositories Module
|
|
3
|
+
*
|
|
4
|
+
* Common utilities and types for repository implementations.
|
|
5
|
+
* TanStack and Storage repositories remain separate but share these utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { createRepositoryKeyFactory } from './domain/RepositoryKeyFactory';
|
|
9
|
+
export type { QueryKeyFactory } from './domain/types';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
mergeRepositoryOptions,
|
|
13
|
+
getCacheOptions,
|
|
14
|
+
normalizeListParams,
|
|
15
|
+
createRepositoryLogger,
|
|
16
|
+
} from './domain/RepositoryUtils';
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
RepositoryOptions,
|
|
20
|
+
ListParams,
|
|
21
|
+
CreateParams,
|
|
22
|
+
UpdateParams,
|
|
23
|
+
} from './domain/types';
|
|
@@ -18,7 +18,10 @@ const getDeviceModule = (): typeof import('expo-device') | null => {
|
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
19
|
_deviceModule = require('expo-device') as typeof import('expo-device');
|
|
20
20
|
return _deviceModule;
|
|
21
|
-
} catch {
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (__DEV__) {
|
|
23
|
+
console.warn('[deviceDetection] expo-device not available:', error);
|
|
24
|
+
}
|
|
22
25
|
return null;
|
|
23
26
|
}
|
|
24
27
|
};
|
|
@@ -35,7 +38,10 @@ export const getScreenDimensions = () => {
|
|
|
35
38
|
try {
|
|
36
39
|
validateScreenDimensions(width, height);
|
|
37
40
|
return { width, height };
|
|
38
|
-
} catch {
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (__DEV__) {
|
|
43
|
+
console.warn('[deviceDetection] Invalid screen dimensions, using fallback:', error);
|
|
44
|
+
}
|
|
39
45
|
return { width: 414, height: 896 };
|
|
40
46
|
}
|
|
41
47
|
};
|
|
@@ -31,7 +31,10 @@ const getHapticsModule = (): typeof import('expo-haptics') | null => {
|
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
32
|
_hapticsModule = require('expo-haptics') as typeof import('expo-haptics');
|
|
33
33
|
return _hapticsModule;
|
|
34
|
-
} catch {
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.warn('[HapticService] expo-haptics not available:', error);
|
|
37
|
+
}
|
|
35
38
|
return null;
|
|
36
39
|
}
|
|
37
40
|
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -20,11 +20,6 @@
|
|
|
20
20
|
|
|
21
21
|
export {
|
|
22
22
|
useAppDesignTokens,
|
|
23
|
-
useDesignSystemTheme,
|
|
24
|
-
useTheme,
|
|
25
|
-
useThemedStyles,
|
|
26
|
-
useThemedStyleSheet,
|
|
27
|
-
useCommonStyles,
|
|
28
23
|
} from '../theme/hooks/useAppDesignTokens';
|
|
29
24
|
|
|
30
25
|
export { useTheme as useThemeStore } from '../theme/infrastructure/stores/themeStore';
|
|
@@ -35,9 +30,10 @@ export { useTheme as useThemeStore } from '../theme/infrastructure/stores/themeS
|
|
|
35
30
|
|
|
36
31
|
export {
|
|
37
32
|
useResponsive,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
useScreenWidth,
|
|
34
|
+
useScreenHeight,
|
|
35
|
+
useScreenDimensions,
|
|
36
|
+
type UseResponsiveReturn,
|
|
41
37
|
} from '../responsive';
|
|
42
38
|
|
|
43
39
|
// =============================================================================
|
|
@@ -45,7 +41,6 @@ export {
|
|
|
45
41
|
// =============================================================================
|
|
46
42
|
|
|
47
43
|
export {
|
|
48
|
-
useSafeArea,
|
|
49
44
|
useSafeAreaInsets,
|
|
50
45
|
} from '../safe-area';
|
|
51
46
|
|
|
@@ -55,8 +50,8 @@ export {
|
|
|
55
50
|
|
|
56
51
|
export {
|
|
57
52
|
useInfiniteScroll,
|
|
58
|
-
type
|
|
59
|
-
type
|
|
53
|
+
type InfiniteScrollConfig,
|
|
54
|
+
type UseInfiniteScrollReturn,
|
|
60
55
|
} from '../infinite-scroll';
|
|
61
56
|
|
|
62
57
|
// =============================================================================
|
|
@@ -65,7 +60,7 @@ export {
|
|
|
65
60
|
|
|
66
61
|
export {
|
|
67
62
|
useOffline,
|
|
68
|
-
type
|
|
63
|
+
type NetworkState,
|
|
69
64
|
} from '../offline';
|
|
70
65
|
|
|
71
66
|
// =============================================================================
|
|
@@ -73,9 +68,11 @@ export {
|
|
|
73
68
|
// =============================================================================
|
|
74
69
|
|
|
75
70
|
export {
|
|
76
|
-
useDeviceContext,
|
|
77
71
|
useDeviceInfo,
|
|
72
|
+
useDeviceCapabilities,
|
|
73
|
+
useDeviceId,
|
|
78
74
|
type DeviceInfo,
|
|
75
|
+
type AnonymousUser,
|
|
79
76
|
} from '../device';
|
|
80
77
|
|
|
81
78
|
// =============================================================================
|
|
@@ -84,8 +81,14 @@ export {
|
|
|
84
81
|
|
|
85
82
|
export {
|
|
86
83
|
useStorage,
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
useStorageState,
|
|
85
|
+
useStore,
|
|
86
|
+
usePersistentCache,
|
|
87
|
+
useCache,
|
|
88
|
+
useCachedValue,
|
|
89
|
+
useCacheState,
|
|
90
|
+
type PersistentCacheOptions,
|
|
91
|
+
type PersistentCacheResult,
|
|
89
92
|
} from '../storage';
|
|
90
93
|
|
|
91
94
|
// =============================================================================
|
|
@@ -93,10 +96,8 @@ export {
|
|
|
93
96
|
// =============================================================================
|
|
94
97
|
|
|
95
98
|
export {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
useCamera,
|
|
99
|
-
type ImagePickerResult,
|
|
99
|
+
useMedia,
|
|
100
|
+
type MediaPickerResult,
|
|
100
101
|
} from '../media';
|
|
101
102
|
|
|
102
103
|
// =============================================================================
|
|
@@ -105,7 +106,6 @@ export {
|
|
|
105
106
|
|
|
106
107
|
export {
|
|
107
108
|
useTimezone,
|
|
108
|
-
useLocalTime,
|
|
109
109
|
type TimezoneInfo,
|
|
110
110
|
} from '../timezone';
|
|
111
111
|
|
|
@@ -115,9 +115,6 @@ export {
|
|
|
115
115
|
|
|
116
116
|
export {
|
|
117
117
|
useHaptics,
|
|
118
|
-
useImpact,
|
|
119
|
-
useNotification,
|
|
120
|
-
useSelection,
|
|
121
118
|
} from '../haptics';
|
|
122
119
|
|
|
123
120
|
// =============================================================================
|
|
@@ -125,6 +122,6 @@ export {
|
|
|
125
122
|
// =============================================================================
|
|
126
123
|
|
|
127
124
|
export {
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
useGlobalLoading,
|
|
126
|
+
type LoadingState,
|
|
130
127
|
} from '../loading';
|
package/src/index.ts
CHANGED
|
@@ -67,33 +67,6 @@ export function useInfiniteScroll<T>(
|
|
|
67
67
|
abortControllerRef.current = new AbortController();
|
|
68
68
|
}, []);
|
|
69
69
|
|
|
70
|
-
const loadInitial = useCallback(async () => {
|
|
71
|
-
if (isLoadingRef.current) return;
|
|
72
|
-
isLoadingRef.current = true;
|
|
73
|
-
cancelPendingRequests();
|
|
74
|
-
|
|
75
|
-
if (isMountedRef.current) setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const newState = await retryWithBackoff(
|
|
79
|
-
() => loadData(config, initialPage, pageSize, totalItems),
|
|
80
|
-
maxRetries,
|
|
81
|
-
retryDelay,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
if (isMountedRef.current) {
|
|
85
|
-
setState(newState);
|
|
86
|
-
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (isMountedRef.current) {
|
|
89
|
-
const errorMessage = error instanceof Error ? error.message : "Failed to load data";
|
|
90
|
-
setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
|
|
91
|
-
}
|
|
92
|
-
} finally {
|
|
93
|
-
isLoadingRef.current = false;
|
|
94
|
-
}
|
|
95
|
-
}, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
|
|
96
|
-
|
|
97
70
|
const loadMore = useCallback(async () => {
|
|
98
71
|
// Get fresh state from ref to avoid stale closure
|
|
99
72
|
const currentState = stateRef.current;
|
|
@@ -165,8 +138,38 @@ export function useInfiniteScroll<T>(
|
|
|
165
138
|
}, [initialPage, totalItems, cancelPendingRequests]);
|
|
166
139
|
|
|
167
140
|
useEffect(() => {
|
|
168
|
-
if (autoLoad)
|
|
169
|
-
|
|
141
|
+
if (autoLoad) {
|
|
142
|
+
const initialize = async () => {
|
|
143
|
+
if (isLoadingRef.current) return;
|
|
144
|
+
|
|
145
|
+
isLoadingRef.current = true;
|
|
146
|
+
cancelPendingRequests();
|
|
147
|
+
|
|
148
|
+
if (isMountedRef.current) setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const newState = await retryWithBackoff(
|
|
152
|
+
() => loadData(config, initialPage, pageSize, totalItems),
|
|
153
|
+
maxRetries,
|
|
154
|
+
retryDelay,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (isMountedRef.current) {
|
|
158
|
+
setState(newState);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (isMountedRef.current) {
|
|
162
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to load data";
|
|
163
|
+
setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
isLoadingRef.current = false;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
initialize();
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
}, [autoLoad]);
|
|
170
173
|
|
|
171
174
|
const canLoadMore = state.hasMore && !state.isLoadingMore && !state.isLoading;
|
|
172
175
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera Picker Strategy
|
|
3
|
+
*
|
|
4
|
+
* Strategy for capturing images/videos using device camera.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
8
|
+
import { PermissionManager } from '../../infrastructure/utils/PermissionManager';
|
|
9
|
+
import type { PickerStrategy, LaunchOptions } from './PickerStrategy';
|
|
10
|
+
import type { MediaLibraryPermission } from '../entities/Media';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Camera picker strategy configuration
|
|
14
|
+
*/
|
|
15
|
+
export interface CameraPickerConfig {
|
|
16
|
+
mediaType: 'images' | 'videos';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Camera picker strategy implementation
|
|
21
|
+
*/
|
|
22
|
+
export class CameraPickerStrategy implements PickerStrategy {
|
|
23
|
+
readonly name = 'CameraPicker';
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private config: CameraPickerConfig,
|
|
27
|
+
private permissionManager: typeof PermissionManager = PermissionManager
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
async getPermission(): Promise<MediaLibraryPermission> {
|
|
31
|
+
return this.permissionManager.requestCameraPermission();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async launch(options: LaunchOptions): Promise<any> {
|
|
35
|
+
const mediaTypes: ImagePicker.MediaTypeOptions =
|
|
36
|
+
this.config.mediaType === 'videos'
|
|
37
|
+
? ImagePicker.MediaTypeOptions.Videos
|
|
38
|
+
: ImagePicker.MediaTypeOptions.Images;
|
|
39
|
+
|
|
40
|
+
const launchOptions: ImagePicker.ImagePickerOptions = {
|
|
41
|
+
mediaTypes,
|
|
42
|
+
allowsEditing: options.allowsEditing ?? false,
|
|
43
|
+
quality: options.quality ?? 1,
|
|
44
|
+
base64: options.base64 ?? false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Add aspect ratio for images only
|
|
48
|
+
if (this.config.mediaType === 'images' && options.aspect) {
|
|
49
|
+
launchOptions.aspect = options.aspect;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add video-specific options
|
|
53
|
+
if (this.config.mediaType === 'videos') {
|
|
54
|
+
if (options.videoMaxDuration !== undefined) {
|
|
55
|
+
launchOptions.videoMaxDuration = options.videoMaxDuration;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return ImagePicker.launchCameraAsync(launchOptions);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library Picker Strategy
|
|
3
|
+
*
|
|
4
|
+
* Strategy for picking images/videos from device gallery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
8
|
+
import { PermissionManager } from '../../infrastructure/utils/PermissionManager';
|
|
9
|
+
import { MediaType } from '../entities/Media';
|
|
10
|
+
import type { PickerStrategy, LaunchOptions } from './PickerStrategy';
|
|
11
|
+
import type { MediaLibraryPermission } from '../entities/Media';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper to map media type to ImagePicker format
|
|
15
|
+
*/
|
|
16
|
+
function mapMediaType(mediaType?: string): ImagePicker.MediaType[] {
|
|
17
|
+
if (!mediaType || mediaType === MediaType.ALL) {
|
|
18
|
+
return ['images', 'videos'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (mediaType === MediaType.VIDEO) {
|
|
22
|
+
return ['videos'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return ['images'];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Library picker strategy implementation
|
|
30
|
+
*/
|
|
31
|
+
export class LibraryPickerStrategy implements PickerStrategy {
|
|
32
|
+
readonly name = 'LibraryPicker';
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private permissionManager: typeof PermissionManager = PermissionManager
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
async getPermission(): Promise<MediaLibraryPermission> {
|
|
39
|
+
return this.permissionManager.requestMediaLibraryPermission();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async launch(options: LaunchOptions): Promise<any> {
|
|
43
|
+
return ImagePicker.launchImageLibraryAsync({
|
|
44
|
+
mediaTypes: mapMediaType(options.mediaTypes),
|
|
45
|
+
allowsEditing: options.allowsEditing ?? false,
|
|
46
|
+
allowsMultipleSelection: options.allowsMultipleSelection ?? false,
|
|
47
|
+
aspect: options.aspect,
|
|
48
|
+
quality: options.quality ?? 1,
|
|
49
|
+
selectionLimit: options.selectionLimit ?? 1,
|
|
50
|
+
base64: options.base64 ?? false,
|
|
51
|
+
exif: options.exif ?? false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Picker Strategy Interface
|
|
3
|
+
*
|
|
4
|
+
* Strategy pattern for different media picker types.
|
|
5
|
+
* Allows MediaPickerService to support camera, video, and library pickers polymorphically.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MediaLibraryPermission } from '../entities/Media';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Common picker options
|
|
12
|
+
*/
|
|
13
|
+
export interface LaunchOptions {
|
|
14
|
+
allowsEditing?: boolean;
|
|
15
|
+
quality?: number;
|
|
16
|
+
aspect?: [number, number];
|
|
17
|
+
videoMaxDuration?: number;
|
|
18
|
+
videoMaxBitrate?: number;
|
|
19
|
+
videoQuality?: 'low' | 'medium' | 'high';
|
|
20
|
+
base64?: boolean;
|
|
21
|
+
exif?: boolean;
|
|
22
|
+
allowsMultipleSelection?: boolean;
|
|
23
|
+
selectionLimit?: number;
|
|
24
|
+
mediaTypes?: string;
|
|
25
|
+
maxFileSizeMB?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Result from picker launch
|
|
30
|
+
*/
|
|
31
|
+
export interface PickerLaunchResult {
|
|
32
|
+
canceled: boolean;
|
|
33
|
+
assets?: Array<{
|
|
34
|
+
uri: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
type?: 'image' | 'video';
|
|
38
|
+
duration?: number;
|
|
39
|
+
fileSize?: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Media picker strategy interface
|
|
45
|
+
*/
|
|
46
|
+
export interface PickerStrategy {
|
|
47
|
+
/**
|
|
48
|
+
* Get required permission for this picker type
|
|
49
|
+
*/
|
|
50
|
+
getPermission(): Promise<MediaLibraryPermission>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Launch the picker with options
|
|
54
|
+
*/
|
|
55
|
+
launch(options: LaunchOptions): Promise<PickerLaunchResult>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Strategy name for debugging
|
|
59
|
+
*/
|
|
60
|
+
readonly name: string;
|
|
61
|
+
}
|