shakebug-react-native 2.4.15 → 2.4.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.
Files changed (33) hide show
  1. package/README.md +6 -0
  2. package/android/src/main/java/com/shakebug/ConnectivityReceiver.java +12 -7
  3. package/android/src/main/java/com/shakebug/DrawingView.java +13 -3
  4. package/android/src/main/java/com/shakebug/RecordScreenModule.java +6 -3
  5. package/android/src/main/java/com/shakebug/ViewShot.java +1 -0
  6. package/ios/CustomScreenShot/ShakebugSDKCustomScreenCapture.m +6 -2
  7. package/ios/ScreenShot/ShakebugCapture.m +3 -1
  8. package/ios/SketchCanvas/DrawingView.m +14 -1
  9. package/package.json +1 -1
  10. package/shakebug-react-native-2.4.15.tgz +0 -0
  11. package/src/APIs/MatchKey.js +1 -1
  12. package/src/Assets/images/shake.png +0 -0
  13. package/src/Assets/images/undo.png +0 -0
  14. package/src/Assets/images/undo@2x.png +0 -0
  15. package/src/Assets/images/undo@3x.png +0 -0
  16. package/src/Componanats/CustomAlert.js +8 -4
  17. package/src/Componanats/Icons.js +278 -0
  18. package/src/Componanats/KeyboardAvoidingWrapper.js +31 -39
  19. package/src/Componanats/LoadingModal.js +7 -4
  20. package/src/Componanats/NavigationContext.js +8 -1
  21. package/src/Componanats/ReviewModal.js +18 -11
  22. package/src/Componanats/SafeAreaHelper.js +70 -0
  23. package/src/Componanats/ShakeFunction.js +655 -364
  24. package/src/Componanats/ShakebugBottomSheet.js +729 -0
  25. package/src/Componanats/Theme.js +96 -0
  26. package/src/Componanats/ZoomableImage.js +350 -0
  27. package/src/Componanats/localization.js +32 -3
  28. package/src/Screens/AddDetailsScreen.js +800 -439
  29. package/src/Screens/FirstScreen.js +950 -540
  30. package/src/Screens/Shakebug.js +78 -44
  31. package/src/Screens/TrimVideoScreen.js +40 -7
  32. package/src/index.tsx +16 -7
  33. package/shakebug-react-native-2.4.14.tgz +0 -0
@@ -1,630 +1,1040 @@
1
- import { Alert, Image, ImageBackground, Linking, PermissionsAndroid, Platform, SafeAreaView, StatusBar, Text, TouchableOpacity, View, TextInput, Keyboard, PanResponder, Animated, StyleSheet, KeyboardAvoidingView, BackHandler, Dimensions, Modal } from 'react-native'
2
- import React, { useEffect, useRef, useState } from 'react'
1
+ import {
2
+ Alert,
3
+ Image,
4
+ ImageBackground,
5
+ Platform,
6
+ SafeAreaView,
7
+ StatusBar,
8
+ Text,
9
+ TouchableOpacity,
10
+ TouchableWithoutFeedback,
11
+ View,
12
+ TextInput,
13
+ Keyboard,
14
+ PanResponder,
15
+ Animated,
16
+ StyleSheet,
17
+ KeyboardAvoidingView,
18
+ BackHandler,
19
+ Dimensions,
20
+ Modal,
21
+ } from 'react-native';
22
+ import React, { useEffect, useRef, useState } from 'react';
3
23
  import { ClearCanvas, RNDrawingView } from './StrockView.js';
4
- import { ShakebugCapture, captureScreen } from '../index';
5
- import { launchImageLibrary } from '../../ImageOrVedioPicker/src/index';
6
- import { logger, consoleTransport } from "react-native-logs";
7
- import { NativeModules } from 'react-native';
24
+ import { ShakebugCapture } from '../index';
8
25
  import { useNavigation } from '../Componanats/NavigationContext';
9
- import { Localization } from '../Componanats/localization';
10
- import { ActivityIndicator } from 'react-native';
11
- import DeviceInfo from '../../DeviceInfo';
26
+ import { Localization, getLocalizedString } from '../Componanats/localization';
12
27
  import { useGlobalState } from '../Componanats/GlobalStateContext.js';
13
28
  import CustomeAlert from '../Componanats/CustomAlert.js';
14
29
  import LoadingModal from '../Componanats/LoadingModal.js';
30
+ import { useTheme } from '../Componanats/Theme.js';
31
+ import { ForwardChevronIcon, CloseCircleIcon } from '../Componanats/Icons.js';
32
+ import { getBottomSpace, isIPhoneWithNotch } from '../Componanats/SafeAreaHelper.js';
15
33
 
16
- console.disableYellowBox = true;
17
- console.ignoredYellowBox = ["Warning: Each", "Warning: Failed", "Possible"];
34
+ const normalizeUri = (uri) => {
35
+ if (!uri || typeof uri !== 'string') return uri;
36
+ if (
37
+ Platform.OS === 'android' &&
38
+ !uri.startsWith('file://') &&
39
+ !uri.startsWith('content://') &&
40
+ !uri.startsWith('http')
41
+ ) {
42
+ return `file://${uri}`;
43
+ }
44
+ return uri;
45
+ };
18
46
 
19
- const FirstScreen = ({ img, themeColor, SDKScreenTitle }) => {
47
+ // Individual Draggable Text Annotation Component
48
+ const DraggableTextItem = ({
49
+ item,
50
+ imageRect,
51
+ isDeleteActive,
52
+ onLongPress,
53
+ onTap,
54
+ onDelete,
55
+ onDismissDelete,
56
+ isLoading,
57
+ }) => {
58
+ const textLayoutRef = useRef({ width: 80, height: 28 });
59
+ const initialYClamped = (() => {
60
+ const cH = imageRect.height || 400;
61
+ const initialY = 0.35 * cH;
62
+ const minY = 4 - initialY;
63
+ const maxY = Math.max(minY, cH - 4 - initialY - 32);
64
+ return Math.max(minY, Math.min(maxY, item.initialOffsetY || 0));
65
+ })();
66
+ const pan = useRef(new Animated.ValueXY({ x: 0, y: initialYClamped })).current;
67
+ const panPosRef = useRef({ x: 0, y: initialYClamped });
68
+ const dragStartPosRef = useRef({ x: 0, y: initialYClamped });
69
+ const longPressTimerRef = useRef(null);
70
+ const isDraggingRef = useRef(false);
71
+ const isLongPressTriggeredRef = useRef(false);
20
72
 
21
- const { setIsScreenShot, setIsRecordingStart } = useGlobalState()
73
+ const getClampedTranslation = (targetX, targetY) => {
74
+ const cW = imageRect.width;
75
+ const cH = imageRect.height;
76
+ const tW = textLayoutRef.current.width || 80;
77
+ const tH = textLayoutRef.current.height || 28;
22
78
 
23
- const ThemeColor = themeColor ? themeColor : "#1dd3e3";
79
+ // Constrain horizontally within image edges (min 4px margin from each side)
80
+ const maxDeltaX = Math.max(0, (cW - tW) / 2 - 4);
81
+ const minX = -maxDeltaX;
82
+ const maxX = maxDeltaX;
24
83
 
25
- const { goBack, navigate, goToScreenAtIndex, focusedScreen } = useNavigation();
84
+ // Constrain vertically within image bounds (base position top: '35%')
85
+ const initialY = 0.35 * cH;
86
+ const minY = 4 - initialY;
87
+ const maxY = Math.max(minY, cH - 4 - initialY - tH);
26
88
 
27
- const [selectd, setSelected] = useState(2)
28
- let canvasRef = useRef()
29
- const ref = useRef();
30
- const [image, setImage] = useState(img);
31
- const [addTextView, setAddTextView] = useState(false)
32
- const [isLoading, setIsLoading] = useState(false)
33
- const [text, setText] = useState("")
34
- const [margin, setMargin] = useState(0)
35
- const [isDraw, setIsDraw] = useState(true)
36
- const [isShow, setIsShow] = useState(false)
37
- const [isAlert, setIsAlert] = useState(false)
38
- const [isAlertText, setIsAlertText] = useState("")
39
- const [isShowModal, setIsShowModal] = useState(true)
89
+ const clampedX = Math.max(minX, Math.min(maxX, targetX));
90
+ const clampedY = Math.max(minY, Math.min(maxY, targetY));
40
91
 
41
- const isFocused = focusedScreen == "AddDetailsScreen"
92
+ return { clampedX, clampedY };
93
+ };
42
94
 
43
- let title = SDKScreenTitle ? SDKScreenTitle.split(",")[0] : ""
95
+ const panResponder = useRef(
96
+ PanResponder.create({
97
+ onStartShouldSetPanResponder: () => true,
98
+ onStartShouldSetPanResponderCapture: () => false,
99
+ onMoveShouldSetPanResponder: (evt, gestureState) => {
100
+ return Math.abs(gestureState.dx) > 3 || Math.abs(gestureState.dy) > 3;
101
+ },
102
+ onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
103
+ return Math.abs(gestureState.dx) > 3 || Math.abs(gestureState.dy) > 3;
104
+ },
105
+ onPanResponderGrant: () => {
106
+ isDraggingRef.current = false;
107
+ isLongPressTriggeredRef.current = false;
108
+ dragStartPosRef.current = { ...panPosRef.current };
44
109
 
45
- useEffect(() => {
46
- StatusBar.setHidden(false, 'slide');
110
+ // 500ms long press timer to reveal remove (X) icon
111
+ if (longPressTimerRef.current) {
112
+ clearTimeout(longPressTimerRef.current);
113
+ }
114
+ longPressTimerRef.current = setTimeout(() => {
115
+ if (!isDraggingRef.current) {
116
+ isLongPressTriggeredRef.current = true;
117
+ onLongPress(item.id);
118
+ }
119
+ }, 500);
120
+ },
121
+ onPanResponderMove: (evt, gestureState) => {
122
+ if (Math.abs(gestureState.dx) > 3 || Math.abs(gestureState.dy) > 3) {
123
+ isDraggingRef.current = true;
124
+ if (longPressTimerRef.current) {
125
+ clearTimeout(longPressTimerRef.current);
126
+ longPressTimerRef.current = null;
127
+ }
128
+ }
129
+ const targetX = dragStartPosRef.current.x + gestureState.dx;
130
+ const targetY = dragStartPosRef.current.y + gestureState.dy;
131
+ const { clampedX, clampedY } = getClampedTranslation(targetX, targetY);
132
+ pan.setValue({ x: clampedX, y: clampedY });
133
+ panPosRef.current = { x: clampedX, y: clampedY };
134
+ },
135
+ onPanResponderRelease: (evt, gestureState) => {
136
+ if (longPressTimerRef.current) {
137
+ clearTimeout(longPressTimerRef.current);
138
+ longPressTimerRef.current = null;
139
+ }
47
140
 
48
- global.isScreenShotDetected = true;
49
- const backAction = () => {
50
- global.isTrigger = false
51
- global.isShakebugOpen = false
52
- global.isScreenShotDetected = false
53
- global.isScreeenShotTaken = false
54
- global.isShakebugSDKOpen = false
55
- setIsScreenShot(false)
56
- setIsRecordingStart(false)
57
- goToScreenAtIndex(0)
58
- return true; // Return true to prevent default behavior (exit app)
59
- };
141
+ if (isLongPressTriggeredRef.current) {
142
+ return;
143
+ }
60
144
 
61
- const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
145
+ // Single tap without dragging
146
+ if (!isDraggingRef.current && Math.abs(gestureState.dx) < 4 && Math.abs(gestureState.dy) < 4) {
147
+ if (isDeleteActive) {
148
+ onDismissDelete();
149
+ } else {
150
+ onTap(item);
151
+ }
152
+ }
153
+ },
154
+ onPanResponderTerminate: () => {
155
+ if (longPressTimerRef.current) {
156
+ clearTimeout(longPressTimerRef.current);
157
+ longPressTimerRef.current = null;
158
+ }
159
+ },
160
+ })
161
+ ).current;
62
162
 
63
- return () => backHandler.remove(); // Clean up the event listener on unmount
163
+ return (
164
+ <Animated.View
165
+ style={[
166
+ styles.textCard,
167
+ {
168
+ transform: [
169
+ { translateX: pan.x },
170
+ { translateY: pan.y },
171
+ ],
172
+ },
173
+ ]}
174
+ >
175
+ <View
176
+ {...panResponder.panHandlers}
177
+ onLayout={(e) => {
178
+ const { width, height } = e.nativeEvent.layout;
179
+ if (width > 0 && height > 0) {
180
+ textLayoutRef.current = { width: width + 10, height: height + 10 };
181
+ }
182
+ }}
183
+ style={[
184
+ styles.textCardBadge,
185
+ {
186
+ borderColor: isDeleteActive ? item.color : 'rgba(0, 0, 0, 0.12)',
187
+ borderWidth: isDeleteActive ? 1.5 : 1,
188
+ },
189
+ ]}
190
+ >
191
+ <Text
192
+ style={[
193
+ styles.annotationText,
194
+ {
195
+ color: item.color,
196
+ },
197
+ ]}
198
+ >
199
+ {item.text}
200
+ </Text>
201
+ </View>
64
202
 
65
- }, []);
203
+ {/* Remove text button (X) */}
204
+ {isDeleteActive && !isLoading ? (
205
+ <TouchableOpacity
206
+ activeOpacity={0.6}
207
+ onPress={() => onDelete(item.id)}
208
+ style={styles.removeTextBtn}
209
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
210
+ >
211
+ <Image
212
+ source={require('../Assets/images/RemoveTextView.png')}
213
+ style={styles.removeIcon}
214
+ resizeMode="contain"
215
+ />
216
+ </TouchableOpacity>
217
+ ) : null}
218
+ </Animated.View>
219
+ );
220
+ };
66
221
 
67
- const { RNCameraPermission } = NativeModules
222
+ const FirstScreen = ({ img, themeColor, SDKScreenTitle }) => {
223
+ const { setIsScreenShot, setIsRecordingStart } = useGlobalState();
224
+ const resolvedColor = (themeColor && themeColor !== '#2E2E2E')
225
+ ? themeColor
226
+ : (global.ShakebugThemeColor || themeColor || '#2E2E2E');
227
+ const ThemeColor = resolvedColor;
228
+ const theme = useTheme(resolvedColor);
229
+ const { navigate, goToScreenAtIndex } = useNavigation();
68
230
 
69
- const [keyboardOpen, setKeyboardOpen] = useState(true);
231
+ // Color selection: 1 = black, 2 = red, 3 = green, 4 = blue, 5 = yellow
232
+ const [selected, setSelected] = useState(1);
233
+ const canvasRef = useRef();
234
+ const ref = useRef();
235
+ const textInputRef = useRef(null);
236
+ const [image, setImage] = useState(img);
237
+ const [addTextView, setAddTextView] = useState(false);
238
+ const [isLoading, setIsLoading] = useState(false);
239
+ const [textItems, setTextItems] = useState([]);
240
+ const [editingItemId, setEditingItemId] = useState(null);
241
+ const [activeDeleteId, setActiveDeleteId] = useState(null);
242
+ const [draftText, setDraftText] = useState('');
243
+ const [isDraw, setIsDraw] = useState(true);
244
+ const [isAlert, setIsAlert] = useState(false);
245
+ const [isAlertText, setIsAlertText] = useState('');
246
+ const bottomInset = getBottomSpace();
247
+ const [imageSize, setImageSize] = useState({
248
+ width: Dimensions.get('screen').width,
249
+ height: Dimensions.get('screen').height,
250
+ });
251
+ const [containerSize, setContainerSize] = useState({
252
+ width: Dimensions.get('window').width,
253
+ height: Dimensions.get('window').height - 100,
254
+ });
70
255
 
71
- const defaultConfig = {
72
- levels: {
73
- debug: 0,
74
- info: 1,
75
- warn: 2,
76
- error: 3,
77
- },
78
- severity: "debug",
79
- transport: consoleTransport,
80
- transportOptions: {
81
- colors: {
82
- info: "blueBright",
83
- warn: "yellowBright",
84
- error: "redBright",
85
- },
86
- },
87
- async: true,
88
- dateFormat: "time",
89
- printLevel: false,
90
- printDate: false,
91
- fixedExtLvlLength: false,
92
- enabled: global.makeLogEnabled == true ? true : false,
93
- };
256
+ // Load exact image aspect ratio
257
+ useEffect(() => {
258
+ if (image) {
259
+ const uri = normalizeUri(image);
260
+ Image.getSize(
261
+ uri,
262
+ (w, h) => {
263
+ if (w > 0 && h > 0) {
264
+ setImageSize({ width: w, height: h });
265
+ }
266
+ },
267
+ () => {
268
+ const { width, height } = Dimensions.get('screen');
269
+ setImageSize({ width, height });
270
+ }
271
+ );
272
+ }
273
+ }, [image]);
94
274
 
95
- var log = logger.createLogger(defaultConfig);
275
+ // Compute exact fitted rectangle of the screenshot so ShakebugCapture captures ONLY the image
276
+ // and drawings/text are strictly bounded within the image
277
+ const getImageRect = () => {
278
+ const maxW = containerSize.width || Dimensions.get('window').width;
279
+ // Leave 80px space for floating toolbar at the bottom so screenshot never overlaps or goes behind it
280
+ const maxH = Math.max(
281
+ 100,
282
+ (containerSize.height || (Dimensions.get('window').height - 100)) - 80
283
+ );
96
284
 
97
- // For geting event while keybord open and close
98
- Keyboard.addListener('keyboardDidShow', () => {
99
- setKeyboardOpen(true);
100
- })
285
+ const imgRatio = (imageSize.width > 0 && imageSize.height > 0)
286
+ ? (imageSize.width / imageSize.height)
287
+ : (Dimensions.get('screen').width / Dimensions.get('screen').height);
101
288
 
102
- Keyboard.addListener('keyboardDidHide', () => {
103
- setKeyboardOpen(false);
104
- })
289
+ let renderW = maxW;
290
+ let renderH = maxW / imgRatio;
105
291
 
106
- UpdateStatusBar = () => {
107
- StatusBar.setHidden(false)
108
- }
292
+ if (renderH > maxH) {
293
+ renderH = maxH;
294
+ renderW = maxH * imgRatio;
295
+ }
109
296
 
110
- // open Video picker to choose video
111
- const openVideoPicker = () => {
112
- setIsLoading(true)
113
- const options = {
114
- mediaType: 'video',
115
- includeBase64: false,
116
- selectionLimit: 1
297
+ return {
298
+ width: Math.round(renderW),
299
+ height: Math.round(renderH),
117
300
  };
301
+ };
118
302
 
119
- launchImageLibrary(options, async (response) => {
120
- if (response.didCancel) {
121
- setIsLoading(false)
122
- } else if (response.error || response.errorCode) {
123
- setIsLoading(false)
124
- // console.log('Image picker error: ', response.error);
125
- } else {
126
- let vedioUri = await response.uri || response.assets?.[0]?.uri;
127
- if (response.assets?.[0]?.type?.slice(0, 5).trim() == "video") {
128
- setIsLoading(false)
129
- navigate("TrimVideoScreen", { video: `${vedioUri}`, Type: "video", themeColor: `${ThemeColor}`, SDKScreenTitle: `${SDKScreenTitle}`, UpdateStatusBar: UpdateStatusBar, updateParent: updateImage })
130
- }
131
- else {
132
- setIsLoading(false)
133
- setIsAlertText("Invalid selection, Please choose a valid video file")
134
- setIsAlert(true)
135
- }
136
- }
137
- });
303
+ const imageRect = getImageRect();
304
+
305
+ // Auto-focus text input reliably on both Android and iOS
306
+ useEffect(() => {
307
+ if (addTextView) {
308
+ const timer = setTimeout(() => {
309
+ textInputRef.current?.focus();
310
+ }, 100);
311
+ return () => clearTimeout(timer);
312
+ }
313
+ }, [addTextView]);
138
314
 
315
+ const title = SDKScreenTitle
316
+ ? SDKScreenTitle.split(',')[0]
317
+ : getLocalizedString('FirstScreenTitle');
318
+
319
+ const handleExit = () => {
320
+ global.isTrigger = false;
321
+ global.isShakebugOpen = false;
322
+ global.isScreenShotDetected = false;
323
+ global.isScreeenShotTaken = false;
324
+ global.isShakebugSDKOpen = false;
325
+ setIsScreenShot(false);
326
+ setIsRecordingStart(false);
327
+ goToScreenAtIndex(0);
139
328
  };
140
329
 
141
- // For update image while change from Add details screen
142
- const updateImage = (data) => {
143
- ClearCanvas(canvasRef)
144
- setText("")
145
- setImage(data)
146
- }
330
+ useEffect(() => {
331
+ StatusBar.setHidden(false, 'slide');
332
+ global.isScreenShotDetected = true;
333
+
334
+ const backAction = () => {
335
+ handleExit();
336
+ return true;
337
+ };
338
+
339
+ const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
340
+ return () => backHandler.remove();
341
+ }, []);
342
+
343
+ const [keyboardOpen, setKeyboardOpen] = useState(true);
147
344
 
148
345
  useEffect(() => {
149
- global.IsBackground = 'false'
150
- Keyboard.addListener('keyboardDidShow', () => {
151
- setMargin("-10%")
152
- })
153
- }, [])
346
+ const showSub = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true));
347
+ const hideSub = Keyboard.addListener('keyboardDidHide', () => setKeyboardOpen(false));
348
+ return () => {
349
+ showSub.remove();
350
+ hideSub.remove();
351
+ };
352
+ }, []);
154
353
 
155
- const ScreenRecord = (ismic) => {
156
- global.isMic = ismic;
157
- global.isRecordingStart = true;
158
- setIsScreenShot(false)
159
- goToScreenAtIndex(0)
160
- }
354
+ const updateImage = (data) => {
355
+ handleClearAll();
356
+ setImage(data);
357
+ };
161
358
 
162
- const RecordVideo = async () => {
163
- Alert.alert(
164
- Localization[global.ShakebugLanguage].RecordAudio,
165
- "",
166
- [
167
- {
168
- text: Localization[global.ShakebugLanguage].No,
169
- onPress: (() => {
170
- setIsLoading(true)
171
- if (Platform.OS == 'ios') {
172
- RNCameraPermission.checkCameraPermissionStatus().then(res => {
173
- if (res == "denied") {
174
- Alert.alert(
175
- "",
176
- Localization[global.ShakebugLanguage].AudioPermission,
177
- [
178
- {
179
- text: Localization[global.ShakebugLanguage].No,
180
- style: 'cancel'
181
- },
182
- {
183
- text: Localization[global.ShakebugLanguage].Yes,
184
- style: 'default',
185
- onPress: (() => {
186
- Linking.openSettings()
187
- })
188
- }
189
- ]
190
- )
191
- }
192
- else if (res == "granted") {
193
- setIsLoading(true)
194
- ScreenRecord(false)
195
- }
196
- })
197
- }
198
- else {
199
- ScreenRecord(false)
200
- }
201
-
202
- }),
203
- style: 'cancel'
204
- },
205
- {
206
- 'text': Localization[global.ShakebugLanguage].Yes,
207
- onPress: (() => {
208
-
209
- if (Platform.OS == 'android') {
210
- PermissionsAndroid.request(
211
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
212
- ).then(res => {
213
- if (res == 'granted') {
214
- setIsLoading(true)
215
- ScreenRecord(true)
216
- }
217
- else if (res == 'denied') {
218
- setIsLoading(true)
219
- ScreenRecord(false)
220
- }
221
- else {
222
- Alert.alert(
223
- "",
224
- Localization[global.ShakebugLanguage].AudioPermission,
225
- [
226
- {
227
- text: Localization[global.ShakebugLanguage].No,
228
- onPress: (() => {
229
- setIsLoading(true)
230
- ScreenRecord(false)
231
- }),
232
- style: 'cancel'
233
- },
234
- {
235
- text: Localization[global.ShakebugLanguage].Yes,
236
- onPress: (() => {
237
- setIsLoading(true)
238
- Linking.openSettings()
239
- })
240
- }
241
- ]
242
- )
243
- }
244
- })
245
- }
246
- else {
247
- setIsLoading(true)
248
- RNCameraPermission.checkCameraPermissionStatus().then(res => {
249
- if (res == "denied") {
250
- Alert.alert(
251
- "",
252
- Localization[global.ShakebugLanguage].AudioPermission,
253
- [
254
- {
255
- text: Localization[global.ShakebugLanguage].No,
256
- style: 'cancel'
257
- },
258
- {
259
- text: Localization[global.ShakebugLanguage].Yes,
260
- style: 'default',
261
- onPress: (() => {
262
- Linking.openSettings()
263
- })
264
- }
265
- ]
266
- )
267
- }
268
- else if (res == "granted") {
269
- setIsLoading(true)
270
- ScreenRecord(true)
271
- }
272
- })
273
- }
274
- })
275
- }
276
- ]
277
- )
359
+ const getColorHex = (id) => {
360
+ switch (id) {
361
+ case 1:
362
+ return '#000000';
363
+ case 2:
364
+ return '#ef4444';
365
+ case 3:
366
+ return '#22c55e';
367
+ case 4:
368
+ return '#3b82f6';
369
+ case 5:
370
+ return '#eab308';
371
+ default:
372
+ return '#000000';
373
+ }
374
+ };
278
375
 
279
- }
376
+ const currentColorHex = getColorHex(selected);
280
377
 
281
- const pan = useRef(new Animated.ValueXY()).current;
378
+ const handleOpenNewText = () => {
379
+ setActiveDeleteId(null);
380
+ setEditingItemId(null);
381
+ setDraftText('');
382
+ setAddTextView(true);
383
+ };
282
384
 
283
- const BOUNDARY_X_MIN = 0;
284
- const BOUNDARY_X_MAX = Dimensions.get('window').width * 0.6; // Set your max boundary for x
285
- const BOUNDARY_Y_MIN = 0;
286
- const BOUNDARY_Y_MAX = Dimensions.get('window').height * 0.75; // Set your max boundary for y
385
+ const handleEditTextItem = (item) => {
386
+ setActiveDeleteId(null);
387
+ setEditingItemId(item.id);
388
+ setDraftText(item.text);
389
+ setAddTextView(true);
390
+ };
287
391
 
288
- const panResponder = useRef(
289
- PanResponder.create({
290
- onStartShouldSetPanResponder: () => true,
291
- onPanResponderMove: (event, gestureState) => {
292
- // Calculate the new x and y values within the boundaries
293
- let newX = Math.max(BOUNDARY_X_MIN, Math.min(BOUNDARY_X_MAX, gestureState.dx + pan.x._offset));
294
- let newY = Math.max(BOUNDARY_Y_MIN, Math.min(BOUNDARY_Y_MAX, gestureState.dy + pan.y._offset));
392
+ const handleDeleteItem = (id) => {
393
+ setTextItems((prev) => prev.filter((it) => it.id !== id));
394
+ if (activeDeleteId === id) {
395
+ setActiveDeleteId(null);
396
+ }
397
+ };
295
398
 
296
- pan.setValue({ x: newX - pan.x._offset, y: newY - pan.y._offset });
297
- },
298
- onPanResponderRelease: () => {
299
- pan.flattenOffset();
300
- },
301
- onPanResponderGrant: () => {
302
- pan.setOffset({
303
- x: pan.x._value,
304
- y: pan.y._value
305
- });
306
- pan.setValue({ x: 0, y: 0 });
399
+ const handleSaveText = () => {
400
+ Keyboard.dismiss();
401
+ const trimmed = draftText.trim();
402
+ if (trimmed.length > 0) {
403
+ if (editingItemId) {
404
+ setTextItems((prev) =>
405
+ prev.map((it) => (it.id === editingItemId ? { ...it, text: trimmed } : it))
406
+ );
407
+ } else {
408
+ const offsets = [0, 45, -45, 90, -90, 135];
409
+ const initialOffsetY = offsets[textItems.length % offsets.length];
410
+ const newItem = {
411
+ id: `text_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
412
+ text: trimmed,
413
+ color: currentColorHex,
414
+ initialOffsetY,
415
+ };
416
+ setTextItems((prev) => [...prev, newItem]);
307
417
  }
308
- })
418
+ } else if (editingItemId) {
419
+ setTextItems((prev) => prev.filter((it) => it.id !== editingItemId));
420
+ }
421
+ setEditingItemId(null);
422
+ setDraftText('');
423
+ setAddTextView(false);
424
+ };
309
425
 
310
- ).current;
426
+ const handleClearAll = () => {
427
+ ClearCanvas(canvasRef);
428
+ setTextItems([]);
429
+ setActiveDeleteId(null);
430
+ setEditingItemId(null);
431
+ setDraftText('');
432
+ };
311
433
 
312
- // backgroundColor: "#181818"
434
+ const handleNext = async () => {
435
+ try {
436
+ setActiveDeleteId(null);
437
+ setIsLoading(true);
438
+ setTimeout(async () => {
439
+ try {
440
+ const ImgUrl = await ref.current.capture();
441
+ setIsLoading(false);
442
+ if (ImgUrl != undefined && ImgUrl != null) {
443
+ global.IsBackground = 'true';
444
+ navigate('AddDetailsScreen', {
445
+ media: `${ImgUrl}`,
446
+ updateParent: updateImage,
447
+ mediaType: 'img',
448
+ themeColor: ThemeColor,
449
+ SDKScreenTitle: `${SDKScreenTitle}`,
450
+ });
451
+ }
452
+ } catch (err) {
453
+ setIsLoading(false);
454
+ }
455
+ }, 60);
456
+ } catch (e) {
457
+ setIsLoading(false);
458
+ }
459
+ };
313
460
 
314
461
  return (
315
- <View style={{ flex: 1, backgroundColor: '#fff' }}>
316
- <StatusBar backgroundColor={ThemeColor} barStyle="dark-content" />
317
- <SafeAreaView style={{ flex: 0, backgroundColor: ThemeColor }} />
318
- {Platform.OS === 'android' && StatusBar.currentHeight ? (
319
- <View style={{ height: StatusBar.currentHeight, backgroundColor: ThemeColor }} />
320
- ) : null}
321
- <SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
322
- <View style={{
323
- flex: 1,
324
- backgroundColor: "#e4fdfd",
325
- paddingBottom: Platform.OS === 'android' ? 48 : 0 // Standard bottom navigation bar height
326
- }}>
327
-
328
- <View style={{ height: '6%', padding: 5, paddingHorizontal: 15, backgroundColor: ThemeColor, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
329
- <TouchableOpacity
330
- style={{
331
- width: '10%',
332
- alignItems: 'center'
333
- }}
334
- onPress={() => {
335
- global.isTrigger = false
336
- global.isShakebugOpen = false
337
- global.IsBackground = 'true'
338
- global.isScreeenShotTaken = false
339
- setIsScreenShot(false)
340
- setIsRecordingStart(false)
341
- goToScreenAtIndex(0)
342
- global.isScreenShotDetected = false
343
- global.isShakebugSDKOpen = false
344
- }}
345
- >
346
- <Image source={require('../Assets/images/close.png')} resizeMode='contain' style={{ width: 35, height: 35 }} />
347
- </TouchableOpacity>
348
- <Text style={{ width: '80%', height: '80%', fontSize: 18, fontWeight: '300', color: 'white', textAlign: 'center' }}>
349
- {title}
350
- </Text>
351
- <TouchableOpacity
352
- style={{
353
- width: '10%',
354
- alignItems: 'center'
355
- }}
356
- onPress={async () => {
357
- const ImgUrl = await ref.current.capture()
358
- if (ImgUrl != undefined || ImgUrl != null) {
359
- global.IsBackground = 'true'
360
- navigate("AddDetailsScreen", { media: `${ImgUrl}`, updateParent: updateImage, mediaType: 'img', themeColor: ThemeColor, SDKScreenTitle: `${SDKScreenTitle}` })
361
- }
362
-
363
- }}
364
- >
365
- <Image source={require('../Assets/images/next.png')} resizeMode='contain' style={{ width: 25, height: 25 }} />
366
- </TouchableOpacity>
367
- </View>
368
- <View style={{ position: 'relative', height: Platform.OS == 'android' ? '78%' : '79%', backgroundColor: '#e4fdfd', width: '100%', alignItems: 'center', paddingTop: 8 }}>
369
- <ShakebugCapture ref={ref} options={{ format: "png", quality: 1 }} collapsable={false} style={{ width: Platform.OS == 'android' ? "77%" : '73%', height: '100%' }}>
462
+ <View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
463
+ <StatusBar backgroundColor={theme.headerBg} barStyle="light-content" />
370
464
 
371
- {text.trim().length > 0 ?
372
- <Animated.View
373
- style={[
374
- styles.card,
375
- {
376
- transform: pan.getTranslateTransform(),
377
- opacity: 1,
378
- borderWidth: isShow ? 1 : 0
379
- },
380
- ]}
465
+ {/* Top Header with Safe Area */}
466
+ <SafeAreaView style={{ backgroundColor: theme.headerBg }}>
467
+ {Platform.OS === 'android' && StatusBar.currentHeight ? (
468
+ <View style={{ height: StatusBar.currentHeight, backgroundColor: theme.headerBg }} />
469
+ ) : null}
470
+ <View style={[styles.header, { backgroundColor: theme.headerBg }]}>
471
+ {/* Close circle button */}
472
+ <TouchableOpacity
473
+ style={styles.headerBtn}
474
+ onPress={handleExit}
475
+ hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
476
+ activeOpacity={0.7}
477
+ >
478
+ <CloseCircleIcon color="#ffffff" size={26} />
479
+ </TouchableOpacity>
381
480
 
382
- >
383
- <TouchableOpacity
384
- style={{
385
- width: '100%',
386
- height: '100%'
387
- }}
388
- activeOpacity={1}
389
- onPress={() => setIsShow(!isShow)}
390
- >
391
- <Text
392
- onLongPress={() => {
393
- setIsShow(!isShow)
394
- }}
395
- {...panResponder.panHandlers}
396
- style={{
397
- fontSize: 20,
398
- fontWeight: 'bold',
399
- backgroundColor: 'white',
400
- borderRadius: 8,
401
- padding: 5,
402
- color: selectd == 1 ? "black" : selectd == 2 ? 'red' : selectd == 3 ? 'green' : selectd == 4 ? '#1dd3e3' : selectd == 5 ? "yellow" : 'black',
403
- }}>{text}</Text>
404
- </TouchableOpacity>
481
+ {/* Screen Title */}
482
+ <Text style={styles.headerTitle} numberOfLines={1}>
483
+ {title || getLocalizedString('AnnotateYourBug')}
484
+ </Text>
405
485
 
406
- {isShow ? <TouchableOpacity
407
- activeOpacity={1}
408
- onPress={() => setText("")}
409
- style={{ position: 'absolute', top: '-25%', left: '-15%', width: 20, height: 20, zIndex: 1 }}>
410
- <Image source={require("../Assets/images/RemoveTextView.png")} style={{ width: '100%', height: '100%' }} resizeMode='contain' />
411
- </TouchableOpacity> : null}
412
- </Animated.View>
413
- : null}
414
- <ImageBackground
415
- source={image ? { uri: image } : undefined}
416
- style={{
417
- alignItems: 'center',
418
- width: "100%",
419
- height: "100%"
420
- }}
421
- resizeMode='contain'
422
- >
486
+ {/* Next chevron button */}
487
+ <TouchableOpacity
488
+ style={styles.headerBtn}
489
+ onPress={handleNext}
490
+ hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
491
+ activeOpacity={0.7}
492
+ >
493
+ <ForwardChevronIcon color="#ffffff" size={22} />
494
+ </TouchableOpacity>
495
+ </View>
496
+ </SafeAreaView>
423
497
 
424
- <RNDrawingView
425
- ref={canvasRef}
426
- style={{ width: '100%', height: '100%' }}
427
- color={selectd == 1 ? "black" : selectd == 2 ? 'red' : selectd == 3 ? 'green' : selectd == 4 ? '#1dd3e3' : selectd == 5 ? "yellow" : 'black'}
428
- strokeWidth={Platform.OS == 'android' ? 5 : 3}
429
- />
498
+ {/* Main Body */}
499
+ <View
500
+ style={[
501
+ styles.bodyContainer,
502
+ {
503
+ backgroundColor: theme.backgroundColor,
504
+ paddingBottom: 0,
505
+ },
506
+ ]}
507
+ onLayout={(e) => {
508
+ const { width, height } = e.nativeEvent.layout;
509
+ if (width > 0 && height > 0) {
510
+ setContainerSize({ width, height });
511
+ }
512
+ }}
513
+ >
514
+ {/* Screenshot Preview Card */}
515
+ <View style={styles.screenshotCard}>
516
+ <ShakebugCapture
517
+ ref={ref}
518
+ options={{ format: 'png', quality: 1 }}
519
+ collapsable={false}
520
+ style={[
521
+ styles.captureContainer,
522
+ {
523
+ width: imageRect.width,
524
+ height: '97%',
525
+ },
526
+ ]}
527
+ >
528
+ <ImageBackground
529
+ source={image ? { uri: normalizeUri(image) } : undefined}
530
+ style={{ width: '100%', height: '97%', marginTop: '3%' }}
531
+ resizeMode="cover"
532
+ >
533
+ <RNDrawingView
534
+ ref={canvasRef}
535
+ style={styles.drawingView}
536
+ color={currentColorHex}
537
+ strokeWidth={Platform.OS === 'android' ? 5 : 3}
538
+ />
539
+ </ImageBackground>
430
540
 
431
- </ImageBackground>
432
- </ShakebugCapture>
541
+ {/* Tap outside to dismiss remove button */}
542
+ {activeDeleteId !== null && (
543
+ <TouchableWithoutFeedback onPress={() => setActiveDeleteId(null)}>
544
+ <View style={[StyleSheet.absoluteFillObject, { zIndex: 10 }]} />
545
+ </TouchableWithoutFeedback>
546
+ )}
433
547
 
434
- </View>
435
- <View style={{ margin: 10, marginHorizontal: 20, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', height: '5%', backgroundColor: "#e4fdfd" }}>
436
- <TouchableOpacity
437
- style={{ backgroundColor: ThemeColor, padding: 8, width: '45%', alignItems: 'center' }}
438
- onPress={() => {
439
- global.IsBackground = 'false'
440
- if (global.AllowVideo == false) {
441
- setIsAlertText(`You have reached your max video limitation. \n\n Used Videos: ${global.used_video} \nAllowed Videos: ${global.total_video}\n\nPlease visit https://www.shakebug.com website and upgrade your plan.`)
442
- setIsAlert(true)
443
- }
444
- else {
445
- DeviceInfo.isEmulator()
446
- .then(res => {
447
- if (res != true || Platform.OS != 'ios') {
448
- RecordVideo()
449
- }
450
- else {
451
- setIsAlertText("Only real device is supported for recording video")
452
- setIsAlert(true)
453
- }
454
- })
455
- }
456
- }}
457
- >
458
- <Text style={{ color: 'white' }}>{Localization[global.ShakebugLanguage].RecordVideo}</Text>
459
- </TouchableOpacity>
460
- <TouchableOpacity
461
- style={{ backgroundColor: ThemeColor, padding: 8, width: '45%', alignItems: 'center' }}
462
- onPress={() => {
463
- if (global.AllowVideo == false) {
464
- setIsAlertText(`You have reached your max video limitation. \n\n Used Videos: ${global.used_video} \nAllowed Videos: ${global.total_video}\n\nPlease visit https://www.shakebug.com website and upgrade your plan.`)
465
- setIsAlert(true)
466
- }
467
- else {
468
- openVideoPicker()
469
- }
470
- }}
471
- >
472
- <Text style={{ color: 'white' }}>{Localization[global.ShakebugLanguage].ChooseVideo}</Text>
473
- </TouchableOpacity>
474
- </View>
548
+ {/* Render all draggable text items */}
549
+ {textItems.map((item) => (
550
+ <DraggableTextItem
551
+ key={item.id}
552
+ item={item}
553
+ imageRect={imageRect}
554
+ isDeleteActive={activeDeleteId === item.id}
555
+ onLongPress={(id) => setActiveDeleteId(id)}
556
+ onTap={handleEditTextItem}
557
+ onDelete={handleDeleteItem}
558
+ onDismissDelete={() => setActiveDeleteId(null)}
559
+ isLoading={isLoading}
560
+ />
561
+ ))}
562
+ </ShakebugCapture>
563
+ </View>
475
564
 
476
- <View style={{ width: "100%", flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 15, height: Platform.OS == 'ios' ? '7%' : '8.5%', paddingVertical: 12, backgroundColor: "#181818" }}>
477
- <TouchableOpacity
478
- onPress={() => {
479
- setIsDraw(true)
480
- setSelected(1)
481
- }}
482
- >
483
- <Image source={selectd == 1 ? require('../Assets/images/borderdBlack.png') : require('../Assets/images/black.png')} resizeMode='contain' style={{ width: 40, height: 40 }} />
484
- </TouchableOpacity>
485
- <TouchableOpacity
486
- onPress={() => setSelected(2)}
487
- >
488
- <Image source={selectd == 2 ? require('../Assets/images/borderdRed.png') : require('../Assets/images/red.png')} resizeMode='contain' style={{ width: 40, height: 40 }} />
489
- </TouchableOpacity>
490
- <TouchableOpacity
491
- onPress={() => setSelected(3)}
492
- >
493
- <Image source={selectd == 3 ? require('../Assets/images/borderdGreen.png') : require('../Assets/images/green.png')} resizeMode='contain' style={{ width: 40, height: 40 }} />
494
- </TouchableOpacity>
495
- <TouchableOpacity
496
- onPress={() => setSelected(4)}
497
- >
498
- <Image source={selectd == 4 ? require('../Assets/images/borderdBlue.png') : require('../Assets/images/blue.png')} resizeMode='contain' style={{ width: 40, height: 40 }} />
499
- </TouchableOpacity>
500
- <TouchableOpacity
501
- onPress={() => setSelected(5)}
502
- >
503
- <Image source={selectd == 5 ? require('../Assets/images/borderdYellow.png') : require('../Assets/images/yellow.png')} resizeMode='contain' style={{ width: 40, height: 40 }} />
504
- </TouchableOpacity>
565
+ {/* Floating Annotation Pill Toolbar matching Reference Image */}
566
+ <View style={styles.floatingToolbar}>
567
+ {[
568
+ { id: 1, color: '#000000' },
569
+ { id: 2, color: '#ef4444' },
570
+ { id: 3, color: '#22c55e' },
571
+ { id: 4, color: '#3b82f6' },
572
+ { id: 5, color: '#eab308' },
573
+ ].map((c) => {
574
+ const isSelected = selected === c.id;
575
+ return (
576
+ <TouchableOpacity
577
+ key={c.id}
578
+ onPress={() => {
579
+ setIsDraw(true);
580
+ setSelected(c.id);
581
+ }}
582
+ style={styles.colorBtn}
583
+ hitSlop={{ top: 8, bottom: 8, left: 4, right: 4 }}
584
+ activeOpacity={0.8}
585
+ >
586
+ {isSelected ? (
587
+ <View style={styles.colorRingActive}>
588
+ <View style={[styles.colorDotActive, { backgroundColor: c.color }]} />
589
+ </View>
590
+ ) : (
591
+ <View style={[styles.colorDot, { backgroundColor: c.color }]} />
592
+ )}
593
+ </TouchableOpacity>
594
+ );
595
+ })}
596
+
597
+ {/* Divider */}
598
+ <View style={styles.toolbarDivider} />
599
+
600
+ {/* Aa Text Button (squircle card) */}
505
601
  <TouchableOpacity
506
- onPress={() => setAddTextView(true)}
602
+ onPress={handleOpenNewText}
603
+ style={styles.textToolBtn}
604
+ hitSlop={{ top: 8, bottom: 8, left: 6, right: 6 }}
605
+ activeOpacity={0.7}
507
606
  >
508
- <Image source={require('../Assets/images/text_icon.png')} resizeMode='contain' style={{ width: 40, height: 40 }} />
509
- </TouchableOpacity>
510
-
511
- <TouchableOpacity style={{ backgroundColor: ThemeColor, padding: 5 }} onPress={() => ClearCanvas(canvasRef)}>
512
- <Image source={require('../Assets/images/clear.png')} resizeMode='contain' style={{ width: 30, height: 30 }} />
607
+ <Text style={styles.textToolLabel}>Aa</Text>
513
608
  </TouchableOpacity>
514
- </View>
515
- </View >
516
609
 
517
- {addTextView ? (<View style={{ flex: 1, position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, backgroundColor: 'black', opacity: 0.93, alignItems: 'center' }}>
518
- <KeyboardAvoidingView
519
- behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
520
- style={styles.container}
521
- >
522
- <View style={styles.innerContainer}>
523
- <TextInput
524
- style={{
525
- width: '90%',
526
- alignSelf: 'center',
527
- height: '70%',
528
- paddingHorizontal: 10,
529
- textAlign: 'center',
530
- textAlignVertical: 'center',
531
- marginTop: 30,
532
- fontSize: 18,
533
- color: selectd == 1 ? "white" : selectd == 2 ? 'red' : selectd == 3 ? 'green' : selectd == 4 ? '#1dd3e3' : selectd == 5 ? "yellow" : 'black'
534
- }}
535
- autoFocus
536
- multiline
537
- placeholder="Type here..."
538
- placeholderTextColor="#b2b2b2"
539
- cursorColor={selectd == 1 ? "white" : selectd == 2 ? 'red' : selectd == 3 ? 'green' : selectd == 4 ? '#1dd3e3' : selectd == 5 ? "yellow" : 'black'}
540
- value={text}
541
- onChangeText={(e) => setText(e)}
542
- />
543
- </View>
610
+ {/* Undo / Clear Drawing Button (squircle card) */}
544
611
  <TouchableOpacity
545
- style={keyboardOpen ? styles.buttonShifted : styles.button}
546
- onPress={() => {
547
- setIsShow(false)
548
- setAddTextView(false)
549
- }}
612
+ onPress={handleClearAll}
613
+ style={styles.undoBtn}
614
+ hitSlop={{ top: 8, bottom: 8, left: 6, right: 6 }}
615
+ activeOpacity={0.7}
550
616
  >
551
- <Text style={{ color: 'white' }}>{Localization[global.ShakebugLanguage].Done}</Text>
617
+ <Image
618
+ source={require('../Assets/images/undo.png')}
619
+ style={styles.undoIcon}
620
+ resizeMode="contain"
621
+ />
552
622
  </TouchableOpacity>
553
- </KeyboardAvoidingView>
554
- </View>) : null}
623
+ </View>
555
624
 
556
625
  <CustomeAlert
557
626
  isVisible={isAlert}
558
627
  text={isAlertText}
559
628
  color={ThemeColor}
560
629
  OnPrees={() => {
561
- setIsAlert(false)
562
- setIsAlertText("")
630
+ setIsAlert(false);
631
+ setIsAlertText('');
563
632
  }}
564
633
  />
565
634
 
566
- </SafeAreaView>
567
- <LoadingModal visible={isLoading} color={ThemeColor} />
635
+ <LoadingModal visible={isLoading} color={ThemeColor} />
636
+ </View>
637
+
638
+ {/* Fullscreen Text Input Overlay when 'Aa' clicked */}
639
+ {addTextView && (
640
+ <View style={styles.textModalBackdrop}>
641
+ <KeyboardAvoidingView
642
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
643
+ style={styles.textModalSafeArea}
644
+ >
645
+ <TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
646
+ <View style={StyleSheet.absoluteFillObject} />
647
+ </TouchableWithoutFeedback>
648
+
649
+ {/* Top Bar with 'Cancel' and 'Done' buttons */}
650
+ <SafeAreaView style={styles.textModalTopBarWrapper}>
651
+ {Platform.OS === 'android' && StatusBar.currentHeight ? (
652
+ <View style={{ height: StatusBar.currentHeight }} />
653
+ ) : null}
654
+ <View style={styles.textModalTopBar}>
655
+ <TouchableOpacity
656
+ style={styles.modalCancelBtn}
657
+ activeOpacity={0.7}
658
+ onPress={() => {
659
+ Keyboard.dismiss();
660
+ setEditingItemId(null);
661
+ setDraftText('');
662
+ setAddTextView(false);
663
+ }}
664
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
665
+ >
666
+ <Text style={styles.modalCancelBtnText}>
667
+ {getLocalizedString('Cancel') || 'Cancel'}
668
+ </Text>
669
+ </TouchableOpacity>
670
+
671
+ <Text style={styles.modalHeaderTitle}>
672
+ {editingItemId ? 'Edit Text' : 'Add Text'}
673
+ </Text>
674
+
675
+ <TouchableOpacity
676
+ style={[
677
+ styles.modalDoneBtn,
678
+ {
679
+ backgroundColor: ThemeColor,
680
+ },
681
+ ]}
682
+ activeOpacity={0.8}
683
+ onPress={handleSaveText}
684
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
685
+ >
686
+ <Text style={styles.modalDoneBtnText}>
687
+ {getLocalizedString('Done') || 'Done'}
688
+ </Text>
689
+ </TouchableOpacity>
690
+ </View>
691
+ </SafeAreaView>
692
+
693
+ {/* Centered Keyboard-avoiding text input */}
694
+ <View style={styles.textModalInputContainer} pointerEvents="box-none">
695
+ <View
696
+ style={[
697
+ styles.textInputCard,
698
+ {
699
+ borderColor:
700
+ currentColorHex === '#000000'
701
+ ? '#334155'
702
+ : currentColorHex,
703
+ },
704
+ ]}
705
+ >
706
+ <TextInput
707
+ ref={textInputRef}
708
+ style={[
709
+ styles.textInputControl,
710
+ {
711
+ color: currentColorHex,
712
+ },
713
+ ]}
714
+ autoFocus
715
+ multiline
716
+ placeholder="Type annotation here..."
717
+ placeholderTextColor="#94a3b8"
718
+ cursorColor={currentColorHex}
719
+ selectionColor={currentColorHex}
720
+ value={draftText}
721
+ onChangeText={(val) => setDraftText(val)}
722
+ />
723
+
724
+ {draftText.length > 0 ? (
725
+ <TouchableOpacity
726
+ onPress={() => setDraftText('')}
727
+ style={styles.clearDraftBtn}
728
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
729
+ >
730
+ <Text style={styles.clearDraftBtnText}>Clear text</Text>
731
+ </TouchableOpacity>
732
+ ) : null}
733
+ </View>
734
+ </View>
735
+ </KeyboardAvoidingView>
736
+ </View>
737
+ )}
568
738
  </View>
569
- )
570
- }
739
+ );
740
+ };
571
741
 
572
742
  const styles = StyleSheet.create({
573
- card: {
574
- width: 'auto',
575
- height: 'auto',
743
+ header: {
744
+ height: 52,
745
+ flexDirection: 'row',
746
+ alignItems: 'center',
747
+ justifyContent: 'space-between',
748
+ paddingHorizontal: 16,
749
+ },
750
+ headerBtn: {
751
+ width: 40,
752
+ height: 40,
576
753
  justifyContent: 'center',
577
- alignItems: 'flex-start',
578
- paddingHorizontal: 6,
754
+ alignItems: 'center',
755
+ },
756
+ closeCircle: {
757
+ width: 32,
758
+ height: 32,
759
+ borderRadius: 16,
760
+ borderWidth: 1.5,
761
+ borderColor: 'rgba(255, 255, 255, 0.75)',
762
+ justifyContent: 'center',
763
+ alignItems: 'center',
764
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
765
+ },
766
+ closeCross: {
767
+ color: '#ffffff',
768
+ fontSize: 14,
769
+ fontWeight: '700',
770
+ marginTop: Platform.OS === 'ios' ? 1 : 0,
771
+ },
772
+ headerTitle: {
773
+ fontSize: 18,
774
+ fontWeight: '700',
775
+ color: '#ffffff',
776
+ textAlign: 'center',
777
+ flex: 1,
778
+ },
779
+ nextChevron: {
780
+ color: '#ffffff',
781
+ fontSize: 34,
782
+ fontWeight: '300',
783
+ textAlign: 'center',
784
+ lineHeight: 38,
785
+ },
786
+ bodyContainer: {
787
+ flex: 1,
788
+ position: 'relative',
789
+ alignItems: 'center',
790
+ paddingBottom: 0,
791
+ },
792
+ screenshotCard: {
793
+ flex: 1,
794
+ width: '100%',
795
+ paddingBottom: 80,
796
+ marginVertical: 0,
797
+ overflow: 'hidden',
798
+ backgroundColor: 'transparent',
799
+ borderWidth: 0,
800
+ alignItems: 'center',
801
+ justifyContent: 'center',
802
+ },
803
+ captureContainer: {
804
+ overflow: 'hidden',
805
+ position: 'relative',
806
+ },
807
+ screenshotImage: {
808
+ width: '100%',
809
+ height: '97%',
810
+ alignItems: 'center',
811
+ justifyContent: 'center',
812
+ backgroundColor: 'transparent',
813
+ },
814
+ drawingView: {
815
+ width: '100%',
816
+ height: '97%'
817
+ },
818
+ floatingToolbar: {
819
+ position: 'absolute',
820
+ bottom: Platform.OS === 'ios' ? (isIPhoneWithNotch() ? 28 : 16) : 54,
821
+ alignSelf: 'center',
822
+ backgroundColor: '#2C3038',
823
+ borderRadius: 24,
824
+ paddingHorizontal: 12,
579
825
  paddingVertical: 6,
580
- borderRadius: 8,
581
- marginBottom: 10,
826
+ flexDirection: 'row',
827
+ alignItems: 'center',
828
+ shadowColor: '#000',
829
+ shadowOffset: { width: 0, height: 4 },
830
+ shadowOpacity: 0.35,
831
+ shadowRadius: 8,
832
+ elevation: 10,
833
+ zIndex: 10,
834
+ },
835
+ colorBtn: {
836
+ width: 34,
837
+ height: 34,
838
+ justifyContent: 'center',
839
+ alignItems: 'center',
840
+ marginHorizontal: 2.5,
841
+ },
842
+ colorDot: {
843
+ width: 32,
844
+ height: 32,
845
+ borderRadius: 16,
846
+ },
847
+ colorRingActive: {
848
+ width: 34,
849
+ height: 34,
850
+ borderRadius: 17,
851
+ borderWidth: 2,
852
+ borderColor: '#ffffff',
853
+ justifyContent: 'center',
854
+ alignItems: 'center',
855
+ backgroundColor: 'transparent',
856
+ },
857
+ colorDotActive: {
858
+ width: 26,
859
+ height: 26,
860
+ borderRadius: 13,
861
+ },
862
+ toolbarDivider: {
863
+ width: 1,
864
+ height: 18,
865
+ backgroundColor: 'rgba(255, 255, 255, 0.18)',
866
+ marginHorizontal: 8,
867
+ },
868
+ textToolBtn: {
869
+ width: 34,
870
+ height: 34,
871
+ borderRadius: 9,
872
+ backgroundColor: '#454950',
873
+ justifyContent: 'center',
874
+ alignItems: 'center',
875
+ },
876
+ textToolLabel: {
877
+ color: '#ffffff',
878
+ fontSize: 16,
879
+ fontWeight: '700',
880
+ },
881
+ undoBtn: {
882
+ width: 34,
883
+ height: 34,
884
+ borderRadius: 9,
885
+ backgroundColor: '#454950',
886
+ justifyContent: 'center',
887
+ alignItems: 'center',
888
+ marginLeft: 6,
889
+ },
890
+ undoIcon: {
891
+ width: 20,
892
+ height: 20,
893
+ tintColor: '#ffffff',
894
+ },
895
+ textCard: {
582
896
  position: 'absolute',
583
- zIndex: 1,
584
- backgroundColor: '#fff',
897
+ top: '35%',
898
+ alignSelf: 'center',
899
+ zIndex: 99,
585
900
  elevation: 8,
901
+ paddingTop: 10,
902
+ paddingRight: 10,
903
+ maxWidth: '85%',
904
+ },
905
+ textCardBadge: {
906
+ backgroundColor: '#ffffff',
907
+ borderRadius: 8,
908
+ paddingHorizontal: 10,
909
+ paddingVertical: 5,
586
910
  shadowColor: '#000',
587
911
  shadowOffset: { width: 0, height: 1 },
588
- shadowOpacity: 0.8,
589
- shadowRadius: 1,
912
+ shadowOpacity: 0.2,
913
+ shadowRadius: 3,
914
+ elevation: 4,
590
915
  },
591
- container: {
592
- width: '100%',
916
+ annotationText: {
917
+ fontSize: 14,
918
+ fontWeight: '600',
919
+ textAlign: 'center',
920
+ lineHeight: 18,
921
+ },
922
+ removeTextBtn: {
923
+ position: 'absolute',
924
+ top: 0,
925
+ right: 0,
926
+ width: 24,
927
+ height: 24,
928
+ borderRadius: 12,
929
+ backgroundColor: '#ffffff',
593
930
  justifyContent: 'center',
594
931
  alignItems: 'center',
932
+ shadowColor: '#000',
933
+ shadowOffset: { width: 0, height: 1 },
934
+ shadowOpacity: 0.25,
935
+ shadowRadius: 2,
936
+ elevation: 12,
937
+ zIndex: 100,
938
+ },
939
+ removeIcon: {
940
+ width: 14,
941
+ height: 14,
942
+ },
943
+ modalHeaderTitle: {
944
+ color: '#ffffff',
945
+ fontSize: 16,
946
+ fontWeight: '700',
947
+ },
948
+ clearDraftBtn: {
949
+ marginTop: 8,
950
+ alignSelf: 'center',
951
+ paddingHorizontal: 12,
952
+ paddingVertical: 4,
953
+ },
954
+ clearDraftBtnText: {
955
+ color: '#94a3b8',
956
+ fontSize: 13,
957
+ fontWeight: '600',
958
+ },
959
+ textModalBackdrop: {
960
+ ...StyleSheet.absoluteFill,
961
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
962
+ zIndex: 99999,
963
+ elevation: 99999,
964
+ },
965
+ textModalSafeArea: {
966
+ flex: 1,
967
+ justifyContent: 'flex-start',
595
968
  },
596
- innerContainer: {
969
+ textModalTopBarWrapper: {
970
+ width: '100%',
971
+ backgroundColor: 'transparent',
972
+ zIndex: 10,
973
+ },
974
+ textModalTopBar: {
975
+ flexDirection: 'row',
597
976
  justifyContent: 'space-between',
598
977
  alignItems: 'center',
599
- width: '100%',
978
+ paddingHorizontal: 16,
979
+ paddingVertical: 12,
600
980
  },
601
- input: {
602
- width: '90%',
603
- height: '80%',
604
- marginBottom: 10,
605
- paddingHorizontal: 10,
606
- textAlign: 'center',
607
- marginTop: 10,
608
- color: "white"
981
+ modalCancelBtn: {
982
+ paddingHorizontal: 14,
983
+ paddingVertical: 8,
609
984
  },
610
- button: {
611
- width: '90%',
612
- marginTop: "20%",
613
- backgroundColor: 'black',
614
- borderWidth: 1,
615
- borderColor: 'white',
616
- padding: 10,
985
+ modalCancelBtnText: {
986
+ color: 'rgba(255, 255, 255, 0.9)',
987
+ fontSize: 16,
988
+ fontWeight: '600',
989
+ },
990
+ modalDoneBtn: {
991
+ paddingHorizontal: 22,
992
+ paddingVertical: 8,
617
993
  borderRadius: 20,
618
- alignItems: 'center'
994
+ alignItems: 'center',
995
+ justifyContent: 'center',
996
+ shadowColor: '#000',
997
+ shadowOffset: { width: 0, height: 2 },
998
+ shadowOpacity: 0.3,
999
+ shadowRadius: 4,
1000
+ elevation: 4,
1001
+ },
1002
+ modalDoneBtnText: {
1003
+ fontSize: 15,
1004
+ fontWeight: '700',
1005
+ color: '#ffffff',
619
1006
  },
620
- buttonShifted: {
1007
+ textModalInputContainer: {
1008
+ flex: 1,
1009
+ justifyContent: 'center',
1010
+ alignItems: 'center',
1011
+ paddingHorizontal: 20,
1012
+ paddingBottom: Platform.OS === 'android' ? 100 : 40,
1013
+ },
1014
+ textInputCard: {
1015
+ backgroundColor: '#ffffff',
1016
+ borderRadius: 16,
1017
+ paddingHorizontal: 20,
1018
+ paddingVertical: 16,
621
1019
  width: '90%',
622
- backgroundColor: 'black',
623
- borderWidth: 1,
624
- borderColor: 'white',
625
- padding: 10,
626
- borderRadius: 20,
627
- alignItems: 'center'
1020
+ maxWidth: 360,
1021
+ minHeight: 100,
1022
+ borderWidth: 2,
1023
+ alignItems: 'center',
1024
+ justifyContent: 'center',
1025
+ shadowColor: '#000',
1026
+ shadowOffset: { width: 0, height: 4 },
1027
+ shadowOpacity: 0.35,
1028
+ shadowRadius: 8,
1029
+ elevation: 8,
1030
+ },
1031
+ textInputControl: {
1032
+ width: '100%',
1033
+ fontSize: 20,
1034
+ fontWeight: '700',
1035
+ textAlign: 'center',
1036
+ paddingVertical: 4,
628
1037
  },
629
1038
  });
630
- export default FirstScreen
1039
+
1040
+ export default FirstScreen;