@chem-po/react-native 0.0.25 → 0.0.27

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.
Files changed (63) hide show
  1. package/lib/commonjs/components/form/UploadProgress/index.js +8 -17
  2. package/lib/commonjs/components/form/UploadProgress/index.js.map +1 -1
  3. package/lib/commonjs/components/form/input/file/index.js +74 -9
  4. package/lib/commonjs/components/form/input/file/index.js.map +1 -1
  5. package/lib/commonjs/components/image/ImageViewModal.js +52 -2
  6. package/lib/commonjs/components/image/ImageViewModal.js.map +1 -1
  7. package/lib/commonjs/components/loading/LoadingImage.js +8 -2
  8. package/lib/commonjs/components/loading/LoadingImage.js.map +1 -1
  9. package/lib/commonjs/components/loading/ProgressBar.js +78 -0
  10. package/lib/commonjs/components/loading/ProgressBar.js.map +1 -0
  11. package/lib/commonjs/components/loading/index.js +11 -0
  12. package/lib/commonjs/components/loading/index.js.map +1 -1
  13. package/lib/commonjs/utils/downloadFile.js +43 -0
  14. package/lib/commonjs/utils/downloadFile.js.map +1 -0
  15. package/lib/module/components/form/UploadProgress/index.js +6 -15
  16. package/lib/module/components/form/UploadProgress/index.js.map +1 -1
  17. package/lib/module/components/form/input/file/index.js +75 -10
  18. package/lib/module/components/form/input/file/index.js.map +1 -1
  19. package/lib/module/components/image/ImageViewModal.js +53 -3
  20. package/lib/module/components/image/ImageViewModal.js.map +1 -1
  21. package/lib/module/components/loading/LoadingImage.js +8 -2
  22. package/lib/module/components/loading/LoadingImage.js.map +1 -1
  23. package/lib/module/components/loading/ProgressBar.js +70 -0
  24. package/lib/module/components/loading/ProgressBar.js.map +1 -0
  25. package/lib/module/components/loading/index.js +1 -0
  26. package/lib/module/components/loading/index.js.map +1 -1
  27. package/lib/module/utils/downloadFile.js +35 -0
  28. package/lib/module/utils/downloadFile.js.map +1 -0
  29. package/lib/typescript/components/form/UploadProgress/index.d.ts.map +1 -1
  30. package/lib/typescript/components/form/input/file/index.d.ts +6 -3
  31. package/lib/typescript/components/form/input/file/index.d.ts.map +1 -1
  32. package/lib/typescript/components/image/ImageViewModal.d.ts +3 -0
  33. package/lib/typescript/components/image/ImageViewModal.d.ts.map +1 -1
  34. package/lib/typescript/components/loading/LoadingImage.d.ts +4 -1
  35. package/lib/typescript/components/loading/LoadingImage.d.ts.map +1 -1
  36. package/lib/typescript/components/loading/ProgressBar.d.ts +7 -0
  37. package/lib/typescript/components/loading/ProgressBar.d.ts.map +1 -0
  38. package/lib/typescript/components/loading/index.d.ts +1 -0
  39. package/lib/typescript/components/loading/index.d.ts.map +1 -1
  40. package/lib/typescript/utils/downloadFile.d.ts +4 -0
  41. package/lib/typescript/utils/downloadFile.d.ts.map +1 -0
  42. package/package.json +4 -3
  43. package/src/components/form/UploadProgress/index.tsx +3 -17
  44. package/src/components/form/input/file/index.tsx +77 -8
  45. package/src/components/image/ImageViewModal.tsx +57 -2
  46. package/src/components/loading/LoadingImage.tsx +16 -1
  47. package/src/components/loading/ProgressBar.tsx +75 -0
  48. package/src/components/loading/index.ts +1 -0
  49. package/src/utils/downloadFile.ts +36 -0
  50. package/lib/commonjs/components/image/ImageViewModal.backup.js +0 -285
  51. package/lib/commonjs/components/image/ImageViewModal.backup.js.map +0 -1
  52. package/lib/commonjs/components/image/ImageViewModal.old.js +0 -285
  53. package/lib/commonjs/components/image/ImageViewModal.old.js.map +0 -1
  54. package/lib/module/components/image/ImageViewModal.backup.js +0 -277
  55. package/lib/module/components/image/ImageViewModal.backup.js.map +0 -1
  56. package/lib/module/components/image/ImageViewModal.old.js +0 -277
  57. package/lib/module/components/image/ImageViewModal.old.js.map +0 -1
  58. package/lib/typescript/components/image/ImageViewModal.backup.d.ts +0 -9
  59. package/lib/typescript/components/image/ImageViewModal.backup.d.ts.map +0 -1
  60. package/lib/typescript/components/image/ImageViewModal.old.d.ts +0 -9
  61. package/lib/typescript/components/image/ImageViewModal.old.d.ts.map +0 -1
  62. package/src/components/image/ImageViewModal.backup.tsx +0 -261
  63. package/src/components/image/ImageViewModal.old.tsx +0 -261
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/UploadProgress/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,OAAO,KAAkB,MAAM,OAAO,CAAA;AAqCtC,eAAO,MAAM,cAAc,GAAI,aAAa;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,sBAwBpE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/UploadProgress/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAkB,MAAM,OAAO,CAAA;AAsCtC,eAAO,MAAM,cAAc,GAAI,aAAa;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,sBAUpE,CAAA"}
@@ -1,12 +1,15 @@
1
- import { FileValue, InputRef } from '@chem-po/core';
1
+ import { FileValue, ImageViewOptions, InputRef } from '@chem-po/core';
2
2
  import { FileField } from '@chem-po/react';
3
3
  import React from 'react';
4
+ import { StyleProp, ViewStyle } from 'react-native';
4
5
  import { FieldProps } from '../../types';
5
- export declare const FileView: ({ value, hasUpload, imageOptions, withFullView, }: {
6
+ export declare const FileView: ({ value, hasUpload, imageOptions, withFullView, nonImageContainerStyle, withDownload, }: {
6
7
  value?: FileValue | null;
7
8
  hasUpload?: boolean;
8
- imageOptions?: any;
9
+ imageOptions?: ImageViewOptions;
9
10
  withFullView?: boolean;
11
+ nonImageContainerStyle?: StyleProp<ViewStyle>;
12
+ withDownload?: boolean;
10
13
  }) => React.JSX.Element;
11
14
  export declare const FileComponent: React.ForwardRefExoticComponent<FieldProps<FileField> & React.RefAttributes<InputRef>>;
12
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/components/form/input/file/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,eAAe,CAAA;AACnE,OAAO,EACL,SAAS,EAMV,MAAM,gBAAgB,CAAA;AAIvB,OAAO,KAA0E,MAAM,OAAO,CAAA;AAG9F,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AA+BxC,eAAO,MAAM,QAAQ,GAAI,mDAKtB;IACD,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACxB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,sBAgCA,CAAA;AAED,eAAO,MAAM,aAAa,wFA2FzB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/components/form/input/file/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAkB,MAAM,eAAe,CAAA;AACrF,OAAO,EACL,SAAS,EAQV,MAAM,gBAAgB,CAAA;AAIvB,OAAO,KAA0E,MAAM,OAAO,CAAA;AAC9F,OAAO,EAAE,SAAS,EAA4C,SAAS,EAAE,MAAM,cAAc,CAAA;AAG7F,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AA+BxC,eAAO,MAAM,QAAQ,GAAI,yFAOtB;IACD,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACxB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,YAAY,CAAC,EAAE,gBAAgB,CAAA;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,sBAAsB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,sBAkFA,CAAA;AAED,eAAO,MAAM,aAAa,wFA2FzB,CAAA"}
@@ -3,6 +3,9 @@ interface ImageViewModalProps {
3
3
  isOpen: boolean;
4
4
  onClose: () => void;
5
5
  src: string | null;
6
+ filename?: string;
7
+ fileType?: string;
8
+ withDownload?: boolean;
6
9
  }
7
10
  export declare const ImageViewModal: React.FC<ImageViewModalProps>;
8
11
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ImageViewModal.d.ts","sourceRoot":"","sources":["../../../../src/components/image/ImageViewModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoD,MAAM,OAAO,CAAA;AAMxE,UAAU,mBAAmB;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAoMxD,CAAA"}
1
+ {"version":3,"file":"ImageViewModal.d.ts","sourceRoot":"","sources":["../../../../src/components/image/ImageViewModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoD,MAAM,OAAO,CAAA;AAOxE,UAAU,mBAAmB;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CA4OxD,CAAA"}
@@ -4,11 +4,14 @@ export interface LoadingImageProps {
4
4
  src?: string | null;
5
5
  loadingOverride?: boolean;
6
6
  onLoad?: (e: any) => void;
7
+ filename?: string;
8
+ fileType?: string;
7
9
  withFullView?: boolean;
8
10
  buttonFullView?: boolean;
9
11
  style?: ViewStyle;
10
12
  width?: DimensionValue;
11
13
  height?: DimensionValue;
14
+ withDownload?: boolean;
12
15
  }
13
- export declare const LoadingImage: ({ src, loadingOverride, onLoad, width, height, withFullView, buttonFullView, style, }: LoadingImageProps) => React.JSX.Element;
16
+ export declare const LoadingImage: ({ src, loadingOverride, onLoad, filename, fileType, width, height, withFullView, buttonFullView, withDownload, style, }: LoadingImageProps) => React.JSX.Element;
14
17
  //# sourceMappingURL=LoadingImage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LoadingImage.d.ts","sourceRoot":"","sources":["../../../../src/components/loading/LoadingImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAChF,OAAO,EAAE,cAAc,EAA6C,SAAS,EAAE,MAAM,cAAc,CAAA;AAInG,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB;AAKD,eAAO,MAAM,YAAY,GAAI,uFAS1B,iBAAiB,sBAmFnB,CAAA"}
1
+ {"version":3,"file":"LoadingImage.d.ts","sourceRoot":"","sources":["../../../../src/components/loading/LoadingImage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAChF,OAAO,EAAE,cAAc,EAA6C,SAAS,EAAE,MAAM,cAAc,CAAA;AAInG,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAKD,eAAO,MAAM,YAAY,GAAI,yHAY1B,iBAAiB,sBA4FnB,CAAA"}
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export declare const ProgressBar: ({ progress, label, height, }: {
3
+ progress: number;
4
+ label?: string;
5
+ height?: number;
6
+ }) => React.JSX.Element;
7
+ //# sourceMappingURL=ProgressBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProgressBar.d.ts","sourceRoot":"","sources":["../../../../src/components/loading/ProgressBar.tsx"],"names":[],"mappings":"AACA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAGhD,eAAO,MAAM,WAAW,GAAI,8BAIzB;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,sBAiCA,CAAA"}
@@ -3,4 +3,5 @@ export * from './Loading';
3
3
  export * from './LoadingImage';
4
4
  export * from './LoadingOverlay';
5
5
  export * from './LoadingSwitch';
6
+ export * from './ProgressBar';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/loading/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/loading/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,4 @@
1
+ export declare const downloadFile: (uri: string, filename: string, fileType: string) => Promise<{
2
+ uri: string;
3
+ } | undefined>;
4
+ //# sourceMappingURL=downloadFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloadFile.d.ts","sourceRoot":"","sources":["../../../src/utils/downloadFile.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,GAAU,KAAK,MAAM,EAAE,UAAU,MAAM,EAAE,UAAU,MAAM;;cAiCjF,CAAA"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@chem-po/react-native",
3
3
  "author": "Elan Canfield",
4
4
  "license": "MIT",
5
- "version": "0.0.25",
5
+ "version": "0.0.27",
6
6
  "main": "lib/commonjs/index.js",
7
7
  "types": "lib/typescript/index.d.ts",
8
8
  "source": "src/index.ts",
@@ -36,6 +36,7 @@
36
36
  "@react-native-community/slider": "4.5.6",
37
37
  "expo-constants": "~17.1.7",
38
38
  "expo-document-picker": "~13.1.6",
39
+ "expo-file-system": "~18.1.11",
39
40
  "expo-image-picker": "~16.1.4",
40
41
  "lottie-react-native": "7.2.2",
41
42
  "nested-property": "^4.0.0",
@@ -50,8 +51,8 @@
50
51
  "react-native-paper-dates": "^0.22.42",
51
52
  "react-native-svg": "15.11.2",
52
53
  "zustand": "^4.3.3",
53
- "@chem-po/core": "0.0.25",
54
- "@chem-po/react": "0.0.25"
54
+ "@chem-po/core": "0.0.27",
55
+ "@chem-po/react": "0.0.27"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@babel/core": "^7.26.0",
@@ -1,7 +1,7 @@
1
1
  import { UploadsState } from '@chem-po/core'
2
- import { useThemeValue } from '@chem-po/react'
3
2
  import React, { useMemo } from 'react'
4
- import { Animated, StyleSheet, Text, View } from 'react-native'
3
+ import { StyleSheet, View } from 'react-native'
4
+ import { ProgressBar } from '../../loading/ProgressBar'
5
5
 
6
6
  const styles = StyleSheet.create({
7
7
  container: {
@@ -40,24 +40,10 @@ const styles = StyleSheet.create({
40
40
  export const UploadProgress = ({ uploads }: { uploads: UploadsState }) => {
41
41
  const asArr = useMemo(() => Object.values(uploads), [uploads])
42
42
 
43
- const accentColor = useThemeValue('colors.accent.300')
44
43
  return (
45
44
  <View style={styles.container}>
46
45
  {asArr.map(upload => (
47
- <Animated.View key={upload.label} style={styles.progressContainer}>
48
- <View style={styles.progressBar}>
49
- <Animated.View
50
- style={[
51
- styles.progressFill,
52
- {
53
- width: `${upload.percent * 100}%`,
54
- backgroundColor: accentColor,
55
- },
56
- ]}
57
- />
58
- </View>
59
- <Text style={styles.label}>{upload.label}</Text>
60
- </Animated.View>
46
+ <ProgressBar key={upload.label} progress={upload.percent} label={upload.label} />
61
47
  ))}
62
48
  </View>
63
49
  )
@@ -1,17 +1,20 @@
1
- import { FileValue, InputRef, LocalFileValue } from '@chem-po/core'
1
+ import { FileValue, ImageViewOptions, InputRef, LocalFileValue } from '@chem-po/core'
2
2
  import {
3
3
  FileField,
4
+ useBackgroundColor,
4
5
  useBorderColor,
5
6
  useIconColor,
6
7
  useObjectUrl,
7
8
  usePlaceholderColor,
8
9
  useTextColor,
10
+ useToast,
9
11
  } from '@chem-po/react'
10
12
  import { Ionicons } from '@expo/vector-icons'
11
13
  import * as DocumentPicker from 'expo-document-picker'
12
14
  import * as ImagePicker from 'expo-image-picker'
13
15
  import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useState } from 'react'
14
- import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
16
+ import { StyleProp, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'
17
+ import { downloadFile } from '../../../../utils/downloadFile'
15
18
  import { LoadingImage } from '../../../loading/LoadingImage'
16
19
  import { FieldProps } from '../../types'
17
20
 
@@ -49,20 +52,57 @@ export const FileView = ({
49
52
  hasUpload,
50
53
  imageOptions,
51
54
  withFullView,
55
+ nonImageContainerStyle,
56
+ withDownload,
52
57
  }: {
53
58
  value?: FileValue | null
54
59
  hasUpload?: boolean
55
- imageOptions?: any
60
+ imageOptions?: ImageViewOptions
56
61
  withFullView?: boolean
62
+ nonImageContainerStyle?: StyleProp<ViewStyle>
63
+ withDownload?: boolean
57
64
  }) => {
58
65
  const { storagePath, dataUrl } = value ?? {}
59
66
  const missingFile = !dataUrl && !storagePath
60
67
 
61
68
  const { url, loading } = useObjectUrl(value)
69
+ const { showError, showSuccess } = useToast()
62
70
 
63
71
  const iconColor = useIconColor()
64
72
  const fileNameColor = useTextColor()
65
73
  const borderColor = useBorderColor()
74
+ const backgroundColor = useBackgroundColor(100)
75
+
76
+ const handleDownload = useCallback(async () => {
77
+ if (!url) {
78
+ showError('File URL is not available')
79
+ return
80
+ }
81
+
82
+ const filename = value?.filename?.split('/').pop()?.replace(/\s+/g, '_')
83
+ if (!filename) {
84
+ showError('Filename is not available')
85
+ return
86
+ }
87
+
88
+ const fileType = value?.type
89
+ if (!fileType) {
90
+ showError('File type is not available')
91
+ return
92
+ }
93
+
94
+ try {
95
+ await downloadFile(url, filename, fileType)
96
+ showSuccess('File downloaded successfully')
97
+ } catch (error: unknown) {
98
+ if (error instanceof Error) {
99
+ console.error('Error downloading file:', error.message)
100
+ } else {
101
+ console.error('Error downloading file:', String(error))
102
+ }
103
+ showError('Failed to download file')
104
+ }
105
+ }, [url, value, showError, showSuccess])
66
106
  if (!value || missingFile) {
67
107
  return <NoFileView hasUpload={hasUpload} />
68
108
  }
@@ -70,7 +110,10 @@ export const FileView = ({
70
110
  if (value.type?.startsWith('image/')) {
71
111
  return (
72
112
  <LoadingImage
113
+ withDownload={withDownload}
114
+ filename={value.filename}
73
115
  src={url}
116
+ fileType={value.type}
74
117
  loadingOverride={loading}
75
118
  width={imageOptions?.width ?? 120}
76
119
  height={imageOptions?.height ?? 120}
@@ -81,9 +124,23 @@ export const FileView = ({
81
124
  }
82
125
 
83
126
  return (
84
- <View style={[styles.fileContainer, { borderColor }]}>
85
- <Ionicons name="document" size={24} color={iconColor} />
86
- <Text style={[styles.filename, { color: fileNameColor }]}>{value.filename}</Text>
127
+ <View style={[styles.fileContainer, { borderColor, backgroundColor }, nonImageContainerStyle]}>
128
+ <View style={styles.iconContainer}>
129
+ <Ionicons name="document" size={24} color={iconColor} />
130
+ </View>
131
+ <View style={styles.filenameContainer}>
132
+ <Text numberOfLines={1} style={[styles.filename, { color: fileNameColor }]}>
133
+ {value.filename}
134
+ </Text>
135
+ </View>
136
+ {withDownload && (
137
+ <TouchableOpacity
138
+ style={styles.downloadButton}
139
+ onPress={() => void handleDownload()}
140
+ disabled={loading}>
141
+ <Ionicons name="download" size={20} color={iconColor} />
142
+ </TouchableOpacity>
143
+ )}
87
144
  </View>
88
145
  )
89
146
  }
@@ -220,13 +277,25 @@ const styles = StyleSheet.create({
220
277
  fileContainer: {
221
278
  flexDirection: 'row',
222
279
  alignItems: 'center',
223
- padding: 8,
224
280
  borderWidth: 1,
225
281
  borderRadius: 4,
282
+ maxWidth: '100%',
283
+ overflow: 'hidden',
284
+ },
285
+ filenameContainer: {
286
+ flex: 1,
287
+ paddingRight: 4,
288
+ justifyContent: 'center',
226
289
  },
227
290
  filename: {
228
291
  fontSize: 14,
229
- marginLeft: 8,
292
+ },
293
+ iconContainer: {
294
+ padding: 4,
295
+ },
296
+ downloadButton: {
297
+ padding: 8,
298
+ marginLeft: 4,
230
299
  },
231
300
  image: {
232
301
  borderRadius: 4,
@@ -1,22 +1,34 @@
1
- import { useScreen } from '@chem-po/react'
1
+ import { useScreen, useToast } from '@chem-po/react'
2
2
  import { Ionicons } from '@expo/vector-icons'
3
3
  import React, { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { Image, Modal, StyleSheet, TouchableOpacity, View } from 'react-native'
5
5
  import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
6
6
  import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'
7
+ import { downloadFile } from '../../utils/downloadFile'
7
8
  import { LoadingLogo } from '../loading/Loading'
8
9
 
9
10
  interface ImageViewModalProps {
10
11
  isOpen: boolean
11
12
  onClose: () => void
12
13
  src: string | null
14
+ filename?: string
15
+ fileType?: string
16
+ withDownload?: boolean
13
17
  }
14
18
 
15
- export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose, src }) => {
19
+ export const ImageViewModal: React.FC<ImageViewModalProps> = ({
20
+ isOpen,
21
+ onClose,
22
+ src,
23
+ filename,
24
+ fileType,
25
+ withDownload,
26
+ }) => {
16
27
  const [loading, setLoading] = useState(true)
17
28
  const screenWidth = useScreen(s => s.width)
18
29
  const screenHeight = useScreen(s => s.height)
19
30
  const [imageSize, setImageSize] = useState({ width: screenWidth / 2, height: screenHeight / 2 })
31
+ const { showError, showSuccess } = useToast()
20
32
 
21
33
  // Shared values for zoom and pan (reanimated)
22
34
  const scale = useSharedValue(1)
@@ -64,6 +76,32 @@ export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose,
64
76
  }
65
77
  }, [isOpen, src, resetTransform])
66
78
 
79
+ // Download functionality
80
+ const handleDownload = useCallback(async () => {
81
+ if (!src) {
82
+ showError('Image URL is not available')
83
+ return
84
+ }
85
+ if (!fileType) {
86
+ showError('File type is not available')
87
+ return
88
+ }
89
+
90
+ try {
91
+ const usedName = filename ?? `image_${Date.now()}.jpg`
92
+
93
+ await downloadFile(src, usedName, fileType)
94
+ showSuccess('Image downloaded successfully')
95
+ } catch (error: unknown) {
96
+ if (error instanceof Error) {
97
+ console.error('Error downloading image:', error.message)
98
+ } else {
99
+ console.error('Error downloading image:', String(error))
100
+ }
101
+ showError('Failed to download image')
102
+ }
103
+ }, [src, showError, showSuccess, filename, fileType])
104
+
67
105
  // Pan gesture with worklet
68
106
  const panGesture = Gesture.Pan()
69
107
  .onUpdate(event => {
@@ -194,6 +232,12 @@ export const ImageViewModal: React.FC<ImageViewModalProps> = ({ isOpen, onClose,
194
232
  </Animated.View>
195
233
  </GestureDetector>
196
234
 
235
+ {withDownload && (
236
+ <TouchableOpacity style={styles.downloadButton} onPress={() => void handleDownload()}>
237
+ <Ionicons name="download" size={24} color="white" />
238
+ </TouchableOpacity>
239
+ )}
240
+
197
241
  <TouchableOpacity style={styles.closeButton} onPress={onClose}>
198
242
  <Ionicons name="close" size={24} color="white" />
199
243
  </TouchableOpacity>
@@ -235,6 +279,17 @@ const styles = StyleSheet.create({
235
279
  width: '100%',
236
280
  height: '100%',
237
281
  },
282
+ downloadButton: {
283
+ position: 'absolute',
284
+ top: 16,
285
+ right: 64,
286
+ width: 40,
287
+ height: 40,
288
+ borderRadius: 20,
289
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
290
+ justifyContent: 'center',
291
+ alignItems: 'center',
292
+ },
238
293
  closeButton: {
239
294
  position: 'absolute',
240
295
  top: 16,
@@ -9,11 +9,14 @@ export interface LoadingImageProps {
9
9
  src?: string | null
10
10
  loadingOverride?: boolean
11
11
  onLoad?: (e: any) => void
12
+ filename?: string
13
+ fileType?: string
12
14
  withFullView?: boolean
13
15
  buttonFullView?: boolean
14
16
  style?: ViewStyle
15
17
  width?: DimensionValue
16
18
  height?: DimensionValue
19
+ withDownload?: boolean
17
20
  }
18
21
 
19
22
  const emptyPng =
@@ -23,10 +26,13 @@ export const LoadingImage = ({
23
26
  src,
24
27
  loadingOverride,
25
28
  onLoad,
29
+ filename,
30
+ fileType,
26
31
  width,
27
32
  height,
28
33
  withFullView,
29
34
  buttonFullView,
35
+ withDownload,
30
36
  style,
31
37
  }: LoadingImageProps) => {
32
38
  const [imageLoading, setImageLoading] = useState(!!src)
@@ -108,7 +114,16 @@ export const LoadingImage = ({
108
114
  ]}>
109
115
  <LoadingLogo isLoading={loading} size={10} />
110
116
  </View>
111
- {viewing && <ImageViewModal isOpen src={src ?? emptyPng} onClose={() => setViewing(false)} />}
117
+ {viewing ? (
118
+ <ImageViewModal
119
+ withDownload={withDownload}
120
+ filename={filename}
121
+ fileType={fileType}
122
+ isOpen
123
+ src={src ?? emptyPng}
124
+ onClose={() => setViewing(false)}
125
+ />
126
+ ) : null}
112
127
  </View>
113
128
  )
114
129
  }
@@ -0,0 +1,75 @@
1
+ import { useColorModeValue, useThemeValue } from '@chem-po/react'
2
+ import React, { useEffect, useRef } from 'react'
3
+ import { Animated, StyleSheet, Text, View } from 'react-native'
4
+
5
+ export const ProgressBar = ({
6
+ progress,
7
+ label,
8
+ height = 26,
9
+ }: {
10
+ progress: number
11
+ label?: string
12
+ height?: number
13
+ }) => {
14
+ const accentColor = useThemeValue('colors.accent.300')
15
+ const animatedWidth = useRef(new Animated.Value(0)).current
16
+ const backgroundColor = useColorModeValue('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')
17
+
18
+ useEffect(() => {
19
+ Animated.timing(animatedWidth, {
20
+ toValue: progress,
21
+ duration: 300,
22
+ useNativeDriver: false, // width animations require layout driver
23
+ }).start()
24
+ }, [progress, animatedWidth])
25
+
26
+ return (
27
+ <View style={[styles.progressContainer, { height, backgroundColor }]}>
28
+ <View style={styles.progressBar}>
29
+ <Animated.View
30
+ style={[
31
+ styles.progressFill,
32
+ {
33
+ width: animatedWidth.interpolate({
34
+ inputRange: [0, 1],
35
+ outputRange: ['0%', '100%'],
36
+ extrapolate: 'clamp',
37
+ }),
38
+ backgroundColor: accentColor,
39
+ },
40
+ ]}
41
+ />
42
+ </View>
43
+ <Text style={styles.label}>{label}</Text>
44
+ </View>
45
+ )
46
+ }
47
+
48
+ const styles = StyleSheet.create({
49
+ progressContainer: {
50
+ width: '100%',
51
+ justifyContent: 'center',
52
+ alignItems: 'center',
53
+ borderRadius: 20,
54
+ padding: 3,
55
+ },
56
+ progressBar: {
57
+ height: '100%',
58
+ width: '100%',
59
+ borderRadius: 20,
60
+ overflow: 'hidden',
61
+ },
62
+ progressFill: {
63
+ height: '100%',
64
+ },
65
+ label: {
66
+ position: 'absolute',
67
+ color: 'white',
68
+ fontSize: 12,
69
+ fontWeight: '500',
70
+ textTransform: 'uppercase',
71
+ textShadowColor: 'rgba(0, 0, 0, 0.67)',
72
+ textShadowOffset: { width: 1, height: 1 },
73
+ textShadowRadius: 3,
74
+ },
75
+ })
@@ -3,3 +3,4 @@ export * from './Loading'
3
3
  export * from './LoadingImage'
4
4
  export * from './LoadingOverlay'
5
5
  export * from './LoadingSwitch'
6
+ export * from './ProgressBar'
@@ -0,0 +1,36 @@
1
+ import * as FileSystem from 'expo-file-system'
2
+
3
+ export const downloadFile = async (uri: string, filename: string, fileType: string) => {
4
+ const permissions = await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync()
5
+ if (!permissions.granted) {
6
+ return
7
+ }
8
+
9
+ // Download to temporary location first
10
+ const tempFileUri = `${FileSystem.cacheDirectory}${filename}`
11
+
12
+ // Download the file to temporary location
13
+ await FileSystem.downloadAsync(uri, tempFileUri)
14
+
15
+ // Read the temporary file as base64
16
+ const base64Content = await FileSystem.readAsStringAsync(tempFileUri, {
17
+ encoding: FileSystem.EncodingType.Base64,
18
+ })
19
+
20
+ // Create a file within the selected directory using SAF
21
+ const fileUri = await FileSystem.StorageAccessFramework.createFileAsync(
22
+ permissions.directoryUri,
23
+ filename,
24
+ fileType,
25
+ )
26
+
27
+ // Write the content to the SAF file
28
+ await FileSystem.StorageAccessFramework.writeAsStringAsync(fileUri, base64Content, {
29
+ encoding: FileSystem.EncodingType.Base64,
30
+ })
31
+
32
+ // Clean up temporary file
33
+ await FileSystem.deleteAsync(tempFileUri, { idempotent: true })
34
+
35
+ return { uri: fileUri }
36
+ }