feat: Implement UI enhancements and fix burst bug
This commit is contained in:
@@ -5,7 +5,7 @@ import CameraScreen from './src/screens/CameraScreen';
|
||||
import PreviewScreen from './src/screens/PreviewScreen';
|
||||
import AdminScreen from './src/screens/AdminScreen';
|
||||
import { loadSettings, saveSettings, AppSettings } from './src/services/settings';
|
||||
import { logger } from './src/services/logger';
|
||||
import { logger, setLogsEnabled } from './src/services/logger';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
|
||||
// Set up global error handler
|
||||
@@ -33,6 +33,7 @@ export default function App() {
|
||||
async function initApp() {
|
||||
const savedSettings = await loadSettings();
|
||||
setSettings(savedSettings);
|
||||
setLogsEnabled(savedSettings.enableLogs);
|
||||
|
||||
// Auto-start Kiosk mode if it was configured as enabled
|
||||
if (savedSettings.kioskModeEnabled) {
|
||||
@@ -129,6 +130,7 @@ export default function App() {
|
||||
|
||||
const handleSaveSettings = (updated: AppSettings) => {
|
||||
setSettings(updated);
|
||||
setLogsEnabled(updated.enableLogs);
|
||||
};
|
||||
|
||||
const handleOpenAdmin = () => {
|
||||
@@ -160,6 +162,8 @@ export default function App() {
|
||||
burstIntervalSec={settings.burstIntervalSec}
|
||||
frameMode={settings.frameMode}
|
||||
selectedFrameId={settings.selectedFrameId}
|
||||
footerText={settings.footerText}
|
||||
useFrontCamera={settings.useFrontCamera}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -200,11 +204,11 @@ export default function App() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#050510',
|
||||
backgroundColor: 'green',
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#050510',
|
||||
backgroundColor: 'red',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
Binary file not shown.
@@ -154,6 +154,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
zIndex: 100,
|
||||
elevation: 21,
|
||||
padding: 30, // Large invisible padding to significantly increase the touch/pinch hitbox
|
||||
},
|
||||
emojiText: {
|
||||
fontSize: 48,
|
||||
@@ -178,8 +179,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
deleteBtn: {
|
||||
position: 'absolute',
|
||||
top: -10,
|
||||
right: -10,
|
||||
top: 15,
|
||||
right: 15,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Switch,
|
||||
Alert,
|
||||
Modal,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
@@ -81,6 +82,56 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount));
|
||||
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
|
||||
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
|
||||
const [footerText, setFooterText] = useState<string>(currentSettings.footerText);
|
||||
const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera);
|
||||
const [enableLogs, setEnableLogs] = useState<boolean>(currentSettings.enableLogs);
|
||||
|
||||
const hasUnsavedChanges = () => {
|
||||
return (
|
||||
countdown !== String(currentSettings.countdownDuration) ||
|
||||
printerIp !== currentSettings.printerIp ||
|
||||
password !== currentSettings.adminPassword ||
|
||||
kioskActive !== currentSettings.kioskModeEnabled ||
|
||||
frameMode !== currentSettings.frameMode ||
|
||||
selectedFrameId !== currentSettings.selectedFrameId ||
|
||||
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
|
||||
dateOverlay !== currentSettings.dateOverlay ||
|
||||
dateOverlayPosition !== currentSettings.dateOverlayPosition ||
|
||||
eventText !== currentSettings.eventText ||
|
||||
burstCount !== String(currentSettings.burstCount) ||
|
||||
burstInterval !== String(currentSettings.burstIntervalSec) ||
|
||||
welcomeText !== currentSettings.welcomeText ||
|
||||
footerText !== currentSettings.footerText ||
|
||||
useFrontCamera !== currentSettings.useFrontCamera ||
|
||||
enableLogs !== currentSettings.enableLogs
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onBackPress = () => {
|
||||
if (hasUnsavedChanges()) {
|
||||
Alert.alert(
|
||||
'Änderungen speichern?',
|
||||
'Du hast ungespeicherte Änderungen. Möchtest du diese vor dem Verlassen speichern?',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{ text: 'Nein (Verwerfen)', onPress: handleClose, style: 'destructive' },
|
||||
{ text: 'Ja (Speichern)', onPress: async () => { await handleSave(); handleClose(); } },
|
||||
]
|
||||
);
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
return true; // Prevent default
|
||||
};
|
||||
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
|
||||
}, [
|
||||
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
|
||||
dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText,
|
||||
footerText, useFrontCamera, enableLogs, currentSettings
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkKioskStatus = () => {
|
||||
@@ -191,6 +242,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
burstCount: burst,
|
||||
burstIntervalSec: interval,
|
||||
welcomeText: welcomeText.trim() || 'Willkommen zu unserer Feier!',
|
||||
footerText: footerText.trim() || 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken',
|
||||
useFrontCamera,
|
||||
enableLogs,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -245,7 +299,21 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
<FontAwesomeIcon icon={faUserGear} size={22} color={THEME.colors.accent} />
|
||||
<Text style={styles.title}>EINSTELLUNGEN</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.closeBtn} onPress={handleClose}>
|
||||
<TouchableOpacity style={styles.closeBtn} onPress={() => {
|
||||
if (hasUnsavedChanges()) {
|
||||
Alert.alert(
|
||||
'Änderungen speichern?',
|
||||
'Möchtest du die Änderungen speichern?',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{ text: 'Verwerfen', onPress: handleClose, style: 'destructive' },
|
||||
{ text: 'Speichern', onPress: async () => { await handleSave(); handleClose(); } },
|
||||
]
|
||||
);
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
|
||||
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -319,6 +387,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
onChangeText={setWelcomeText}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Subtext (Footer)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="ZUM STARTEN TIPPEN..."
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={footerText}
|
||||
onChangeText={setFooterText}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Event-Name (für Datum-Sticker)</Text>
|
||||
<TextInput
|
||||
@@ -361,6 +439,24 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Tablet-Frontkamera als Fallback erzwingen</Text>
|
||||
<Switch
|
||||
value={useFrontCamera}
|
||||
onValueChange={setUseFrontCamera}
|
||||
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
|
||||
thumbColor={THEME.colors.text}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>App-Protokollierung (Logs) aktivieren</Text>
|
||||
<Switch
|
||||
value={enableLogs}
|
||||
onValueChange={setEnableLogs}
|
||||
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
|
||||
thumbColor={THEME.colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
|
||||
@@ -51,9 +51,11 @@ interface CameraScreenProps {
|
||||
adminPassword?: string;
|
||||
welcomeText?: string;
|
||||
burstCount?: number;
|
||||
burstIntervalSec?: number;
|
||||
frameMode?: 'off' | 'always' | 'available';
|
||||
selectedFrameId?: string | null;
|
||||
burstIntervalSec: number;
|
||||
frameMode: 'off' | 'always' | 'available';
|
||||
selectedFrameId: string | null;
|
||||
footerText: string;
|
||||
useFrontCamera: boolean;
|
||||
}
|
||||
|
||||
export default function CameraScreen({
|
||||
@@ -67,20 +69,23 @@ export default function CameraScreen({
|
||||
adminPassword = '1234',
|
||||
welcomeText = 'Willkommen zu unserer Feier!',
|
||||
burstCount = 1,
|
||||
burstIntervalSec = 5,
|
||||
frameMode = 'off',
|
||||
selectedFrameId = null,
|
||||
burstIntervalSec,
|
||||
frameMode,
|
||||
selectedFrameId,
|
||||
footerText,
|
||||
useFrontCamera,
|
||||
}: CameraScreenProps) {
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
|
||||
const [countdown, setCountdown] = useState<number | string>('');
|
||||
const [countdown, setCountdown] = useState<number | 'smile'>(countdownDuration);
|
||||
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
||||
const [hasStarted, setHasStarted] = useState<boolean>(false);
|
||||
const [pictureSize, setPictureSize] = useState<string | undefined>(undefined);
|
||||
|
||||
// Burst mode state
|
||||
// Burst mode state and refs
|
||||
const [burstIndex, setBurstIndex] = useState<number>(0);
|
||||
const [burstUris, setBurstUris] = useState<string[]>([]);
|
||||
const burstIndexRef = useRef<number>(0);
|
||||
const burstUrisRef = useRef<string[]>([]);
|
||||
const [showBurstIndicator, setShowBurstIndicator] = useState<string>('');
|
||||
const [isBurstWaiting, setIsBurstWaiting] = useState<boolean>(false);
|
||||
|
||||
@@ -228,14 +233,15 @@ export default function CameraScreen({
|
||||
if (burstTimerRef.current) clearTimeout(burstTimerRef.current);
|
||||
if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current);
|
||||
if (completionTimerRef.current) clearTimeout(completionTimerRef.current);
|
||||
const urisToDelete = [...burstUris];
|
||||
const urisToDelete = [...burstUrisRef.current];
|
||||
|
||||
setCountdown(countdownDuration);
|
||||
setIsCapturing(false);
|
||||
setIsBurstWaiting(false);
|
||||
setHasStarted(false);
|
||||
setBurstIndex(0);
|
||||
setBurstUris([]);
|
||||
burstIndexRef.current = 0;
|
||||
burstUrisRef.current = [];
|
||||
setShowBurstIndicator('');
|
||||
onCancel();
|
||||
|
||||
@@ -254,7 +260,8 @@ export default function CameraScreen({
|
||||
if (isIdle) {
|
||||
setHasStarted(false);
|
||||
setBurstIndex(0);
|
||||
setBurstUris([]);
|
||||
burstIndexRef.current = 0;
|
||||
burstUrisRef.current = [];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -361,13 +368,14 @@ export default function CameraScreen({
|
||||
|
||||
// Handle burst mode
|
||||
const effectiveBurst = burstCount || 1;
|
||||
const newIndex = burstIndex + 1;
|
||||
const newUris = [...burstUris, capturedUri];
|
||||
const newIndex = burstIndexRef.current + 1;
|
||||
const newUris = [...burstUrisRef.current, capturedUri];
|
||||
|
||||
if (effectiveBurst > 1 && newIndex < effectiveBurst) {
|
||||
// More burst photos to take
|
||||
burstIndexRef.current = newIndex;
|
||||
burstUrisRef.current = newUris;
|
||||
setBurstIndex(newIndex);
|
||||
setBurstUris(newUris);
|
||||
setShowBurstIndicator(`Foto ${newIndex} von ${effectiveBurst} ✓`);
|
||||
setIsCapturing(false);
|
||||
setIsBurstWaiting(true);
|
||||
@@ -379,8 +387,9 @@ export default function CameraScreen({
|
||||
}, burstIntervalSec * 1000);
|
||||
} else if (effectiveBurst > 1) {
|
||||
// Final burst photo
|
||||
burstIndexRef.current = 0;
|
||||
burstUrisRef.current = [];
|
||||
setBurstIndex(0);
|
||||
setBurstUris([]);
|
||||
// Short delay prevents Android expo-camera crash on immediate unmount
|
||||
completionTimerRef.current = setTimeout(() => {
|
||||
onBurstComplete(newUris);
|
||||
@@ -512,7 +521,7 @@ export default function CameraScreen({
|
||||
adjustsFontSizeToFit
|
||||
numberOfLines={1}
|
||||
>
|
||||
ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken
|
||||
{footerText}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -644,7 +653,7 @@ export default function CameraScreen({
|
||||
{/* Camera Preview Background */}
|
||||
<View style={styles.previewContainer}>
|
||||
<View style={styles.cameraWrapper}>
|
||||
{Platform.OS === 'android' && isUsbConnected ? (
|
||||
{Platform.OS === 'android' && !useFrontCamera ? (
|
||||
<UsbCameraView
|
||||
ref={usbCameraRef}
|
||||
style={[styles.cameraPreview, { position: 'absolute', opacity: 1, zIndex: 10 }]}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
BackHandler,
|
||||
} from 'react-native';
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
@@ -96,6 +97,18 @@ export default function PreviewScreen({
|
||||
// Idle Timer state
|
||||
const idleTimerRef = useRef<any>(null);
|
||||
|
||||
// Hardware Back Press
|
||||
useEffect(() => {
|
||||
const onBackPress = () => {
|
||||
onReset();
|
||||
return true; // Prevent default behavior (closing the app)
|
||||
};
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
||||
return () => {
|
||||
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
|
||||
};
|
||||
}, [onReset]);
|
||||
|
||||
// Frame state
|
||||
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(
|
||||
frameMode === 'always' ? initialFrameId : null
|
||||
@@ -106,7 +119,7 @@ export default function PreviewScreen({
|
||||
const currentPhoto = photoUris[photoUris.length - 1];
|
||||
|
||||
// Get the active frame asset
|
||||
const activeFrame = selectedFrameId && photoUris.length === 1
|
||||
const activeFrame = selectedFrameId
|
||||
? FRAMES.find((f) => f.id === selectedFrameId)
|
||||
: null;
|
||||
|
||||
@@ -578,7 +591,7 @@ export default function PreviewScreen({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{frameMode === 'available' && photoUris.length === 1 && (
|
||||
{frameMode === 'available' && (
|
||||
<TouchableOpacity
|
||||
style={[styles.editToolBtn, selectedFrameId !== null && styles.editToolBtnActive]}
|
||||
onPress={() => {
|
||||
@@ -603,8 +616,8 @@ export default function PreviewScreen({
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Frame Picker (when mode = available and only 1 photo) */}
|
||||
{frameMode === 'available' && photoUris.length === 1 && (
|
||||
{/* Frame Picker (when mode = available) */}
|
||||
{frameMode === 'available' && (
|
||||
<FramePicker
|
||||
selectedFrameId={selectedFrameId}
|
||||
onSelectFrame={(id) => {
|
||||
|
||||
@@ -5,6 +5,11 @@ const logFileUri = FileSystem.documentDirectory + 'app_logs.txt';
|
||||
// Queue to serialize file operations and prevent race conditions
|
||||
const logQueue: string[] = [];
|
||||
let isWriting = false;
|
||||
let isEnabled = true;
|
||||
|
||||
export const setLogsEnabled = (enabled: boolean) => {
|
||||
isEnabled = enabled;
|
||||
};
|
||||
|
||||
const processLogQueue = async () => {
|
||||
if (isWriting) return;
|
||||
@@ -48,6 +53,7 @@ const enqueueWrite = async (logLine: string) => {
|
||||
|
||||
export const logger = {
|
||||
log: async (message: string) => {
|
||||
if (!isEnabled) return;
|
||||
const timestamp = new Date().toISOString();
|
||||
const logLine = `[INFO] ${timestamp}: ${message}\n`;
|
||||
console.log(logLine.trim());
|
||||
@@ -55,6 +61,10 @@ export const logger = {
|
||||
},
|
||||
|
||||
error: async (message: string, error?: any) => {
|
||||
if (!isEnabled) {
|
||||
console.error(`[ERROR]: ${message}`, error);
|
||||
return;
|
||||
}
|
||||
const timestamp = new Date().toISOString();
|
||||
let errorString = '';
|
||||
if (error) {
|
||||
|
||||
@@ -21,6 +21,10 @@ export interface AppSettings {
|
||||
burstIntervalSec: number; // seconds between burst shots
|
||||
// Welcome screen text
|
||||
welcomeText: string;
|
||||
footerText: string;
|
||||
// System
|
||||
useFrontCamera: boolean;
|
||||
enableLogs: boolean;
|
||||
}
|
||||
|
||||
const settingsFile = new File(Paths.document, 'settings.json');
|
||||
@@ -39,6 +43,9 @@ const DEFAULT_SETTINGS: AppSettings = {
|
||||
burstCount: 1,
|
||||
burstIntervalSec: 5,
|
||||
welcomeText: 'Willkommen zu unserer Feier!',
|
||||
footerText: 'ZUM STARTEN TIPPEN • Fotos machen • Collage • Drucken',
|
||||
useFrontCamera: false,
|
||||
enableLogs: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user