Skip to content

Commit d6b8fdb

Browse files
committed
feat(example): add EXIF overlay, camera placeholder, UI polish
- Add ExifOverlay component with image preview, URI, and tag list - Add camera placeholder for iOS simulator - Add URL preview button with last-input persistence - Fix iOS build: update NativeExifySpec to NativeExifyModuleSpec - Promote true-sheet in README
1 parent cb8847c commit d6b8fdb

6 files changed

Lines changed: 238 additions & 20 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ console.log(result.tags)
6565

6666
See [example](example) for more detailed usage.
6767

68+
### Built with True Sheet
69+
70+
The example app uses [@lodev09/react-native-true-sheet](https://github.com/lodev09/react-native-true-sheet) for the image picker UI. Check it out!
71+
6872
## Contributing
6973

7074
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

example/src/App.tsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import {
1313
ImagePickerSheet,
1414
type ImagePickerSheetRef,
1515
} from './components/ImagePickerSheet';
16+
import { ExifOverlay } from './components/ExifOverlay';
1617

1718
export default function App() {
1819
const cameraRef = useRef<CameraView>(null);
1920
const promptRef = useRef<PromptSheetRef>(null);
2021
const pickerRef = useRef<ImagePickerSheetRef>(null);
2122
const [preview, setPreview] = useState<string>();
23+
const [exifTags, setExifTags] = useState<ExifTags | null>(null);
24+
const [exifUri, setExifUri] = useState<string>();
25+
const [urlPreview, setUrlPreview] = useState<string>();
2226

2327
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
2428
const [mediaPermission, requestMediaPermission] =
@@ -32,6 +36,8 @@ export default function App() {
3236
const readExif = async (uri: string) => {
3337
const tags = await Exify.read(uri);
3438
console.log('readExif:', json(tags));
39+
setExifUri(uri);
40+
setExifTags(tags);
3541
return tags;
3642
};
3743

@@ -117,19 +123,21 @@ export default function App() {
117123
await readWriteRoundTrip(uri);
118124
};
119125

120-
const openUrl = async () => {
121-
const defaultUrl =
122-
'https://raw.githubusercontent.com/ianare/exif-samples/master/jpg/gps/DSCN0010.jpg';
126+
const lastUrlRef = useRef(
127+
'https://raw.githubusercontent.com/ianare/exif-samples/master/jpg/gps/DSCN0010.jpg'
128+
);
123129

130+
const openUrl = async () => {
124131
const url = await promptRef.current?.prompt(
125132
'Enter image URL',
126-
defaultUrl,
133+
lastUrlRef.current,
127134
'https://'
128135
);
129136
if (!url) return;
130137

131138
console.log('openUrl:', url);
132-
// setPreview(url);
139+
lastUrlRef.current = url;
140+
setUrlPreview(url);
133141
await readExif(url);
134142
};
135143

@@ -153,7 +161,22 @@ export default function App() {
153161

154162
return (
155163
<View style={styles.container}>
156-
<CameraView ref={cameraRef} style={styles.camera} facing="back" />
164+
<View style={styles.camera}>
165+
<View style={styles.cameraPlaceholder}>
166+
<Text style={styles.cameraIcon}>📷</Text>
167+
<Text style={styles.cameraLabel}>Camera</Text>
168+
</View>
169+
<CameraView
170+
style={StyleSheet.absoluteFill}
171+
facing="back"
172+
ref={cameraRef}
173+
/>
174+
<ExifOverlay
175+
uri={exifUri}
176+
tags={exifTags}
177+
onClose={() => setExifTags(null)}
178+
/>
179+
</View>
157180
<PromptSheet ref={promptRef} />
158181
<ImagePickerSheet ref={pickerRef} />
159182
<View style={styles.controls}>
@@ -162,15 +185,21 @@ export default function App() {
162185
onLongPress={testRoundTrip}
163186
style={styles.sideButton}
164187
>
165-
{preview && (
188+
{preview ? (
166189
<Image source={{ uri: preview }} style={styles.preview} />
190+
) : (
191+
<Text style={styles.buttonIcon}>🏞️</Text>
167192
)}
168193
</Pressable>
169194
<Pressable onPress={takePhoto} style={styles.captureButton}>
170195
<View style={styles.captureInner} />
171196
</Pressable>
172197
<Pressable onPress={openUrl} style={styles.sideButton}>
173-
<Text style={styles.urlLabel}>URL</Text>
198+
{urlPreview ? (
199+
<Image source={{ uri: urlPreview }} style={styles.preview} />
200+
) : (
201+
<Text style={styles.urlLabel}>URL</Text>
202+
)}
174203
</Pressable>
175204
</View>
176205
</View>
@@ -192,6 +221,20 @@ const styles = StyleSheet.create({
192221
flex: 1,
193222
width: '100%',
194223
},
224+
cameraPlaceholder: {
225+
...StyleSheet.absoluteFillObject,
226+
alignItems: 'center',
227+
justifyContent: 'center',
228+
backgroundColor: '#1a1a1a',
229+
},
230+
cameraIcon: {
231+
fontSize: 80,
232+
marginBottom: 12,
233+
},
234+
cameraLabel: {
235+
color: 'rgba(255, 255, 255, 0.3)',
236+
fontSize: 15,
237+
},
195238
controls: {
196239
flexDirection: 'row',
197240
alignItems: 'center',
@@ -207,8 +250,6 @@ const styles = StyleSheet.create({
207250
height: 50,
208251
borderRadius: 8,
209252
backgroundColor: '#333',
210-
borderWidth: 1,
211-
borderColor: '#555',
212253
alignItems: 'center',
213254
justifyContent: 'center',
214255
overflow: 'hidden',
@@ -232,6 +273,9 @@ const styles = StyleSheet.create({
232273
borderRadius: 29,
233274
backgroundColor: '#fff',
234275
},
276+
buttonIcon: {
277+
fontSize: 24,
278+
},
235279
urlLabel: {
236280
color: '#fff',
237281
fontSize: 13,
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import {
2+
Platform,
3+
Pressable,
4+
ScrollView,
5+
StyleSheet,
6+
Text,
7+
View,
8+
} from 'react-native';
9+
import { Image } from 'expo-image';
10+
11+
import type { ExifTags } from '@lodev09/react-native-exify';
12+
13+
interface ExifOverlayProps {
14+
uri?: string;
15+
tags: ExifTags | null;
16+
onClose: () => void;
17+
}
18+
19+
const formatValue = (value: unknown): string => {
20+
if (Array.isArray(value)) return value.join(', ');
21+
if (typeof value === 'number')
22+
return String(Math.round(value * 1000000) / 1000000);
23+
return String(value);
24+
};
25+
26+
export const ExifOverlay = ({ uri, tags, onClose }: ExifOverlayProps) => {
27+
if (!tags) return null;
28+
29+
const entries = Object.entries(tags).sort(([a], [b]) => a.localeCompare(b));
30+
31+
return (
32+
<View style={styles.backdrop}>
33+
<View style={styles.panel}>
34+
<View style={styles.header}>
35+
<Text style={styles.title}>EXIF</Text>
36+
<View style={styles.headerRight}>
37+
<Text style={styles.count}>{entries.length} tags</Text>
38+
<Pressable style={styles.closeButton} onPress={onClose}>
39+
<View style={styles.closeIcon}>
40+
<Text style={styles.closeIconText}></Text>
41+
</View>
42+
</Pressable>
43+
</View>
44+
</View>
45+
{uri && (
46+
<Image source={{ uri }} style={styles.preview} contentFit="cover" />
47+
)}
48+
{uri && (
49+
<Text style={styles.uri} numberOfLines={2} selectable>
50+
{uri}
51+
</Text>
52+
)}
53+
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
54+
{entries.map(([key, value]) => (
55+
<View key={key} style={styles.row}>
56+
<Text style={styles.key} numberOfLines={1}>
57+
{key}
58+
</Text>
59+
<Text style={styles.value} numberOfLines={2} selectable>
60+
{formatValue(value)}
61+
</Text>
62+
</View>
63+
))}
64+
</ScrollView>
65+
</View>
66+
</View>
67+
);
68+
};
69+
70+
const styles = StyleSheet.create({
71+
backdrop: {
72+
...StyleSheet.absoluteFillObject,
73+
justifyContent: 'center',
74+
padding: 20,
75+
},
76+
panel: {
77+
flex: 1,
78+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
79+
borderRadius: 16,
80+
overflow: 'hidden',
81+
marginTop: 60,
82+
marginBottom: 20,
83+
},
84+
header: {
85+
flexDirection: 'row',
86+
alignItems: 'center',
87+
justifyContent: 'space-between',
88+
paddingHorizontal: 16,
89+
paddingVertical: 12,
90+
borderBottomWidth: StyleSheet.hairlineWidth,
91+
borderBottomColor: 'rgba(255, 255, 255, 0.15)',
92+
},
93+
title: {
94+
color: '#fff',
95+
fontSize: 15,
96+
fontWeight: '700',
97+
letterSpacing: 1,
98+
},
99+
headerRight: {
100+
flexDirection: 'row',
101+
alignItems: 'center',
102+
gap: 10,
103+
},
104+
count: {
105+
color: 'rgba(255, 255, 255, 0.4)',
106+
fontSize: 13,
107+
},
108+
closeButton: {
109+
padding: 2,
110+
},
111+
closeIcon: {
112+
width: 26,
113+
height: 26,
114+
borderRadius: 13,
115+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
116+
alignItems: 'center',
117+
justifyContent: 'center',
118+
},
119+
closeIconText: {
120+
color: 'rgba(255, 255, 255, 0.6)',
121+
fontSize: 12,
122+
fontWeight: '600',
123+
},
124+
preview: {
125+
height: 120,
126+
borderBottomWidth: StyleSheet.hairlineWidth,
127+
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
128+
},
129+
uri: {
130+
color: 'rgba(255, 255, 255, 0.35)',
131+
fontSize: 11,
132+
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace' }),
133+
paddingHorizontal: 16,
134+
paddingVertical: 8,
135+
borderBottomWidth: StyleSheet.hairlineWidth,
136+
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
137+
},
138+
scroll: {
139+
flex: 1,
140+
},
141+
row: {
142+
flexDirection: 'row',
143+
paddingHorizontal: 16,
144+
paddingVertical: 8,
145+
borderBottomWidth: StyleSheet.hairlineWidth,
146+
borderBottomColor: 'rgba(255, 255, 255, 0.06)',
147+
},
148+
key: {
149+
width: 140,
150+
color: 'rgba(255, 255, 255, 0.5)',
151+
fontSize: 12,
152+
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace' }),
153+
},
154+
value: {
155+
flex: 1,
156+
color: '#fff',
157+
fontSize: 12,
158+
fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace' }),
159+
},
160+
});

example/src/components/ImagePickerSheet.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@ export const ImagePickerSheet = forwardRef<ImagePickerSheetRef>(
122122
<View style={styles.header}>
123123
<Text style={styles.title}>Recents</Text>
124124
<Pressable
125-
style={styles.cancelButton}
125+
style={styles.closeButton}
126126
onPress={() => sheetRef.current?.dismiss()}
127127
>
128-
<Text style={styles.cancelText}>Cancel</Text>
128+
<View style={styles.closeIcon}>
129+
<Text style={styles.closeIconText}></Text>
130+
</View>
129131
</Pressable>
130132
</View>
131133
),
@@ -172,13 +174,21 @@ const styles = StyleSheet.create({
172174
fontSize: 17,
173175
fontWeight: '600',
174176
},
175-
cancelButton: {
176-
paddingVertical: 4,
177-
paddingHorizontal: 8,
177+
closeButton: {
178+
padding: 4,
178179
},
179-
cancelText: {
180-
color: '#0a84ff',
181-
fontSize: 17,
180+
closeIcon: {
181+
width: 28,
182+
height: 28,
183+
borderRadius: 14,
184+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
185+
alignItems: 'center',
186+
justifyContent: 'center',
187+
},
188+
closeIconText: {
189+
color: 'rgba(255, 255, 255, 0.6)',
190+
fontSize: 13,
191+
fontWeight: '600',
182192
},
183193
row: {
184194
gap: GAP,

ios/Exify.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
#import <React/RCTLog.h>
55
#import <UIKit/UIKit.h>
66

7-
@interface Exify : NSObject <NativeExifySpec>
7+
@interface Exify : NSObject <NativeExifyModuleSpec>
88

99
@end

ios/Exify.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ - (void)write:(NSString *)uri
377377

378378
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
379379
(const facebook::react::ObjCTurboModule::InitParams &)params {
380-
return std::make_shared<facebook::react::NativeExifySpecJSI>(params);
380+
return std::make_shared<facebook::react::NativeExifyModuleSpecJSI>(params);
381381
}
382382

383383
+ (NSString *)moduleName {

0 commit comments

Comments
 (0)