Compare commits
2
Commits
e2c4a48c91
...
faa584ae0f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa584ae0f | ||
|
|
cb453bf200 |
@@ -73,6 +73,7 @@ export default function CameraScreen({
|
|||||||
const [countdown, setCountdown] = useState<number | string>('');
|
const [countdown, setCountdown] = useState<number | string>('');
|
||||||
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
||||||
const [hasStarted, setHasStarted] = useState<boolean>(false);
|
const [hasStarted, setHasStarted] = useState<boolean>(false);
|
||||||
|
const [pictureSize, setPictureSize] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
// Burst mode state
|
// Burst mode state
|
||||||
const [burstIndex, setBurstIndex] = useState<number>(0);
|
const [burstIndex, setBurstIndex] = useState<number>(0);
|
||||||
@@ -91,6 +92,7 @@ export default function CameraScreen({
|
|||||||
const usbCameraRef = useRef<UsbCameraRef>(null);
|
const usbCameraRef = useRef<UsbCameraRef>(null);
|
||||||
const expoCameraRef = useRef<any>(null);
|
const expoCameraRef = useRef<any>(null);
|
||||||
const burstTimerRef = useRef<any>(null);
|
const burstTimerRef = useRef<any>(null);
|
||||||
|
const isCancelledRef = useRef<boolean>(false);
|
||||||
|
|
||||||
// Reanimated countdown pulse animation
|
// Reanimated countdown pulse animation
|
||||||
const scale = useSharedValue(1);
|
const scale = useSharedValue(1);
|
||||||
@@ -158,8 +160,43 @@ export default function CameraScreen({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Set safe picture size to avoid OOM
|
||||||
|
const handleCameraReady = async () => {
|
||||||
|
if (expoCameraRef.current) {
|
||||||
|
try {
|
||||||
|
const sizes = await expoCameraRef.current.getAvailablePictureSizes();
|
||||||
|
if (sizes && sizes.length > 0) {
|
||||||
|
console.log('Available camera sizes:', sizes);
|
||||||
|
const parsedSizes = sizes.map(s => {
|
||||||
|
const parts = s.split('x');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const w = parseInt(parts[0], 10);
|
||||||
|
const h = parseInt(parts[1], 10);
|
||||||
|
return { size: s, pixels: w * h, w, h };
|
||||||
|
}
|
||||||
|
return { size: s, pixels: 0, w: 0, h: 0 };
|
||||||
|
}).filter(s => s.pixels > 0);
|
||||||
|
|
||||||
|
parsedSizes.sort((a, b) => a.pixels - b.pixels);
|
||||||
|
// Find the smallest size that has at least 1920 width or height (approx 2MP)
|
||||||
|
const optimal = parsedSizes.find(s => Math.max(s.w, s.h) >= 1920);
|
||||||
|
|
||||||
|
if (optimal) {
|
||||||
|
console.log('Setting optimal pictureSize to prevent OOM:', optimal.size);
|
||||||
|
setPictureSize(optimal.size);
|
||||||
|
} else if (parsedSizes.length > 0) {
|
||||||
|
setPictureSize(parsedSizes[parsedSizes.length - 1].size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fetch picture sizes:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Cancel capture process
|
// Cancel capture process
|
||||||
const handleLocalCancel = async () => {
|
const handleLocalCancel = async () => {
|
||||||
|
isCancelledRef.current = true;
|
||||||
if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
|
if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
|
||||||
|
|
||||||
// Clean up any temp files from a partially completed burst
|
// Clean up any temp files from a partially completed burst
|
||||||
@@ -173,6 +210,8 @@ export default function CameraScreen({
|
|||||||
|
|
||||||
setCountdown(countdownDuration);
|
setCountdown(countdownDuration);
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
|
setIsBurstWaiting(false);
|
||||||
|
setHasStarted(false);
|
||||||
setBurstIndex(0);
|
setBurstIndex(0);
|
||||||
setBurstUris([]);
|
setBurstUris([]);
|
||||||
setShowBurstIndicator('');
|
setShowBurstIndicator('');
|
||||||
@@ -194,6 +233,7 @@ export default function CameraScreen({
|
|||||||
}, [countdownDuration, isIdle]);
|
}, [countdownDuration, isIdle]);
|
||||||
|
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
|
isCancelledRef.current = false;
|
||||||
let timer: any;
|
let timer: any;
|
||||||
let count = countdownDuration;
|
let count = countdownDuration;
|
||||||
|
|
||||||
@@ -246,7 +286,7 @@ export default function CameraScreen({
|
|||||||
if (expoCameraRef.current) {
|
if (expoCameraRef.current) {
|
||||||
const photo = await expoCameraRef.current.takePictureAsync({
|
const photo = await expoCameraRef.current.takePictureAsync({
|
||||||
quality: 0.7, // Lower quality slightly to reduce file size
|
quality: 0.7, // Lower quality slightly to reduce file size
|
||||||
skipProcessing: false, // Ensure correct orientation
|
skipProcessing: Platform.OS === 'android', // Prevent memory crash on Android
|
||||||
});
|
});
|
||||||
capturedUri = photo.uri;
|
capturedUri = photo.uri;
|
||||||
} else {
|
} else {
|
||||||
@@ -254,6 +294,12 @@ export default function CameraScreen({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If user cancelled while the camera promise was processing, abort before side-effects
|
||||||
|
if (isCancelledRef.current) {
|
||||||
|
console.log('Capture cancelled by user during processing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Instead of manipulating the image and risking an OOM or BitmapFactory crash,
|
// Instead of manipulating the image and risking an OOM or BitmapFactory crash,
|
||||||
// we directly use the captured photo and rely on React Native's Image component
|
// we directly use the captured photo and rely on React Native's Image component
|
||||||
// to safely downsample it during rendering via resizeMethod="resize".
|
// to safely downsample it during rendering via resizeMethod="resize".
|
||||||
@@ -503,6 +549,8 @@ export default function CameraScreen({
|
|||||||
ref={expoCameraRef}
|
ref={expoCameraRef}
|
||||||
style={styles.cameraPreview}
|
style={styles.cameraPreview}
|
||||||
facing="front"
|
facing="front"
|
||||||
|
pictureSize={pictureSize}
|
||||||
|
onCameraReady={handleCameraReady}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export default function PreviewScreen({
|
|||||||
|
|
||||||
// Triggers the view capture and the print job
|
// Triggers the view capture and the print job
|
||||||
const handlePrint = async () => {
|
const handlePrint = async () => {
|
||||||
|
if (isProcessing) return;
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setStatusMessage('Dein Foto wird vorbereitet...');
|
setStatusMessage('Dein Foto wird vorbereitet...');
|
||||||
try {
|
try {
|
||||||
@@ -233,6 +234,7 @@ export default function PreviewScreen({
|
|||||||
|
|
||||||
// Saves to gallery without printing
|
// Saves to gallery without printing
|
||||||
const handleSaveOnly = async () => {
|
const handleSaveOnly = async () => {
|
||||||
|
if (isProcessing) return;
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setStatusMessage('Bild wird generiert...');
|
setStatusMessage('Bild wird generiert...');
|
||||||
try {
|
try {
|
||||||
@@ -255,6 +257,7 @@ export default function PreviewScreen({
|
|||||||
|
|
||||||
// Auto-save and exit
|
// Auto-save and exit
|
||||||
const handleExit = useCallback(async () => {
|
const handleExit = useCallback(async () => {
|
||||||
|
if (isProcessing) return;
|
||||||
const state = stateRef.current;
|
const state = stateRef.current;
|
||||||
if (state.stickers.length === 0 && state.layout === 'single' && !state.activeFrame && state.dateOverlay === 'off') {
|
if (state.stickers.length === 0 && state.layout === 'single' && !state.activeFrame && state.dateOverlay === 'off') {
|
||||||
state.onReset();
|
state.onReset();
|
||||||
@@ -294,6 +297,7 @@ export default function PreviewScreen({
|
|||||||
}, [resetIdleTimer]);
|
}, [resetIdleTimer]);
|
||||||
|
|
||||||
const handleRetakeClick = async () => {
|
const handleRetakeClick = async () => {
|
||||||
|
if (isProcessing) return;
|
||||||
if (stickers.length > 0 || layout !== 'single' || activeFrame || dateOverlay !== 'off') {
|
if (stickers.length > 0 || layout !== 'single' || activeFrame || dateOverlay !== 'off') {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setStatusMessage('Wird vor dem Wiederholen gespeichert...');
|
setStatusMessage('Wird vor dem Wiederholen gespeichert...');
|
||||||
@@ -523,7 +527,7 @@ export default function PreviewScreen({
|
|||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.printBtn]} onPress={handlePrint} activeOpacity={0.8}>
|
<TouchableOpacity style={[styles.actionBtn, styles.printBtn, isProcessing && styles.btnDisabled]} onPress={handlePrint} activeOpacity={0.8} disabled={isProcessing}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={THEME.gradient.primary}
|
colors={THEME.gradient.primary}
|
||||||
start={{ x: 0, y: 0.5 }}
|
start={{ x: 0, y: 0.5 }}
|
||||||
@@ -535,25 +539,25 @@ export default function PreviewScreen({
|
|||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn]} onPress={handleSaveOnly}>
|
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn, isProcessing && styles.btnDisabled]} onPress={handleSaveOnly} disabled={isProcessing}>
|
||||||
<FontAwesomeIcon icon={faDownload} size={16} color={THEME.colors.accent} style={{ marginRight: 8 }} />
|
<FontAwesomeIcon icon={faDownload} size={16} color={THEME.colors.accent} style={{ marginRight: 8 }} />
|
||||||
<Text style={[styles.actionBtnText, { color: THEME.colors.accent }]}>Nur auf Tablet speichern</Text>
|
<Text style={[styles.actionBtnText, { color: THEME.colors.accent }]}>Nur auf Tablet speichern</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{photoUris.length < 4 && (
|
{photoUris.length < 4 && (
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.addBtn]} onPress={onAddAnother}>
|
<TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}>
|
||||||
<FontAwesomeIcon icon={faPlus} size={16} color={THEME.colors.text} style={{ marginRight: 8 }} />
|
<FontAwesomeIcon icon={faPlus} size={16} color={THEME.colors.text} style={{ marginRight: 8 }} />
|
||||||
<Text style={styles.actionBtnText}>Weiteres Foto aufnehmen</Text>
|
<Text style={styles.actionBtnText}>Weiteres Foto aufnehmen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.rowActions}>
|
<View style={styles.rowActions}>
|
||||||
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn]} onPress={handleRetakeClick}>
|
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn, isProcessing && styles.btnDisabled]} onPress={handleRetakeClick} disabled={isProcessing}>
|
||||||
<FontAwesomeIcon icon={faRotateLeft} size={14} color={THEME.colors.error} style={{ marginRight: 6 }} />
|
<FontAwesomeIcon icon={faRotateLeft} size={14} color={THEME.colors.error} style={{ marginRight: 6 }} />
|
||||||
<Text style={[styles.smallBtnText, { color: THEME.colors.error }]}>Wiederholen</Text>
|
<Text style={[styles.smallBtnText, { color: THEME.colors.error }]}>Wiederholen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn]} onPress={handleExit}>
|
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn, isProcessing && styles.btnDisabled]} onPress={handleExit} disabled={isProcessing}>
|
||||||
<FontAwesomeIcon icon={faRightFromBracket} size={14} color={THEME.colors.textMuted} style={{ marginRight: 6 }} />
|
<FontAwesomeIcon icon={faRightFromBracket} size={14} color={THEME.colors.textMuted} style={{ marginRight: 6 }} />
|
||||||
<Text style={styles.smallBtnText}>Beenden</Text>
|
<Text style={styles.smallBtnText}>Beenden</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -747,6 +751,9 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
btnDisabled: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
// ── Photo Canvases ──
|
// ── Photo Canvases ──
|
||||||
singleCanvas: {
|
singleCanvas: {
|
||||||
width: 480,
|
width: 480,
|
||||||
|
|||||||
Reference in New Issue
Block a user