Add on-device logger
This commit is contained in:
@@ -4,7 +4,15 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|||||||
import CameraScreen from './src/screens/CameraScreen';
|
import CameraScreen from './src/screens/CameraScreen';
|
||||||
import PreviewScreen from './src/screens/PreviewScreen';
|
import PreviewScreen from './src/screens/PreviewScreen';
|
||||||
import AdminScreen from './src/screens/AdminScreen';
|
import AdminScreen from './src/screens/AdminScreen';
|
||||||
import { loadSettings, AppSettings } from './src/services/settings';
|
import { loadSettings, saveSettings, AppSettings } from './src/services/settings';
|
||||||
|
import { logger } from './src/services/logger';
|
||||||
|
|
||||||
|
// Set up global error handler
|
||||||
|
const defaultErrorHandler = (global as any).ErrorUtils.getGlobalHandler();
|
||||||
|
(global as any).ErrorUtils.setGlobalHandler(async (error: any, isFatal: boolean) => {
|
||||||
|
await logger.error(`Unhandled Exception (Fatal: ${isFatal})`, error);
|
||||||
|
defaultErrorHandler(error, isFatal);
|
||||||
|
});
|
||||||
import KioskMode from './modules/kiosk-mode';
|
import KioskMode from './modules/kiosk-mode';
|
||||||
|
|
||||||
type ScreenState = 'home' | 'camera' | 'preview' | 'admin';
|
type ScreenState = 'home' | 'camera' | 'preview' | 'admin';
|
||||||
@@ -42,9 +50,15 @@ export default function App() {
|
|||||||
setCurrentScreen('camera');
|
setCurrentScreen('camera');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePhotoCaptured = (uri: string) => {
|
const handlePhotoCaptured = async (uri: string) => {
|
||||||
setPhotoUris((prev) => [...prev, uri]);
|
await logger.log(`Photo captured via CameraScreen: ${uri}`);
|
||||||
setCurrentScreen('preview');
|
try {
|
||||||
|
setPhotoUris((prev) => [...prev, uri]);
|
||||||
|
setCurrentScreen('preview');
|
||||||
|
await logger.log('State updated to preview screen successfully.');
|
||||||
|
} catch (e) {
|
||||||
|
await logger.error('Error transitioning to preview screen:', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Called by CameraScreen when burst mode completes all photos
|
// Called by CameraScreen when burst mode completes all photos
|
||||||
|
|||||||
+44
-12
@@ -8,6 +8,7 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Switch,
|
Switch,
|
||||||
Alert,
|
Alert,
|
||||||
|
Modal,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
@@ -22,9 +23,10 @@ import {
|
|||||||
faCalendarDay,
|
faCalendarDay,
|
||||||
faImages,
|
faImages,
|
||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faChampagneGlasses,
|
faFileLines,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { THEME } from '../styles/theme';
|
import { THEME } from '../styles/theme';
|
||||||
|
import { logger } from '../services/logger';
|
||||||
import KioskMode from '../../modules/kiosk-mode';
|
import KioskMode from '../../modules/kiosk-mode';
|
||||||
import { AppSettings, saveSettings } from '../services/settings';
|
import { AppSettings, saveSettings } from '../services/settings';
|
||||||
import { FRAMES } from '../data/frames';
|
import { FRAMES } from '../data/frames';
|
||||||
@@ -63,11 +65,11 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
|
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
|
||||||
const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp);
|
const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp);
|
||||||
const [password, setPassword] = useState<string>(currentSettings.adminPassword);
|
const [password, setPassword] = useState<string>(currentSettings.adminPassword);
|
||||||
|
const [logs, setLogs] = useState<string | null>(null);
|
||||||
|
|
||||||
const [kioskActive, setKioskActive] = useState<boolean>(false);
|
const [kioskActive, setKioskActive] = useState<boolean>(false);
|
||||||
const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false);
|
const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false);
|
||||||
|
|
||||||
// New settings state
|
|
||||||
const [frameMode, setFrameMode] = useState<FrameMode>(currentSettings.frameMode);
|
const [frameMode, setFrameMode] = useState<FrameMode>(currentSettings.frameMode);
|
||||||
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
|
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
|
||||||
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
|
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
|
||||||
@@ -78,7 +80,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
|
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
|
||||||
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
|
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
|
||||||
|
|
||||||
// Load Kiosk and Device Owner status on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkKioskStatus = () => {
|
const checkKioskStatus = () => {
|
||||||
try {
|
try {
|
||||||
@@ -92,7 +93,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkKioskStatus();
|
checkKioskStatus();
|
||||||
// Poll status every second
|
|
||||||
const interval = setInterval(checkKioskStatus, 1000);
|
const interval = setInterval(checkKioskStatus, 1000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -196,7 +196,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Renders a 3-way toggle segment control
|
|
||||||
const renderSegment = <T extends string>(
|
const renderSegment = <T extends string>(
|
||||||
options: { value: T; label: string }[],
|
options: { value: T; label: string }[],
|
||||||
current: T,
|
current: T,
|
||||||
@@ -228,10 +227,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
|
<FontAwesomeIcon icon={faArrowLeft} size={14} color={THEME.colors.text} />
|
||||||
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
|
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.logBtn}
|
||||||
|
onPress={async () => setLogs(await logger.readLogs())}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFileLines} size={16} color={THEME.colors.text} />
|
||||||
|
<Text style={styles.closeBtnText}>Logs</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
{/* ─── Printer Configuration ─── */}
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} />
|
||||||
@@ -253,7 +258,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ─── Booth Behavior ─── */}
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
|
||||||
@@ -304,7 +308,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ─── Continuous Shooting ─── */}
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
|
||||||
@@ -337,7 +340,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ─── Photo Frames ─── */}
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
|
||||||
@@ -414,7 +416,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ─── Date/Time Overlay ─── */}
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} />
|
||||||
@@ -433,7 +434,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ─── Kiosk Mode ─── */}
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
|
||||||
@@ -474,7 +474,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ─── Save Button ─── */}
|
|
||||||
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
|
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={THEME.gradient.primary}
|
colors={THEME.gradient.primary}
|
||||||
@@ -487,6 +486,32 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Log Modal */}
|
||||||
|
<Modal visible={logs !== null} transparent animationType="slide">
|
||||||
|
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<View style={{ width: '90%', height: '85%', backgroundColor: '#222', borderRadius: 10, padding: 20 }}>
|
||||||
|
<Text style={{ fontSize: 24, fontWeight: 'bold', color: '#fff', marginBottom: 10 }}>App Logs</Text>
|
||||||
|
<ScrollView style={{ flex: 1, backgroundColor: '#000', padding: 10, borderRadius: 5 }}>
|
||||||
|
<Text style={{ color: '#0f0', fontFamily: 'monospace' }}>{logs}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 15 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ backgroundColor: THEME.colors.primary, padding: 15, borderRadius: 8 }}
|
||||||
|
onPress={() => setLogs(null)}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Schließen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ backgroundColor: THEME.colors.danger, padding: 15, borderRadius: 8 }}
|
||||||
|
onPress={async () => { await logger.clearLogs(); setLogs(await logger.readLogs()); }}
|
||||||
|
>
|
||||||
|
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Leeren</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -707,4 +732,11 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
letterSpacing: 2,
|
letterSpacing: 2,
|
||||||
},
|
},
|
||||||
|
logBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 10,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { THEME } from '../styles/theme';
|
import { THEME } from '../styles/theme';
|
||||||
import { printImageLocal } from '../services/printer';
|
import { printImageLocal } from '../services/printer';
|
||||||
import { saveToGallery } from '../services/storage';
|
import { saveToGallery } from '../services/storage';
|
||||||
|
import { logger } from '../services/logger';
|
||||||
import DraggableSticker, { StickerData } from '../components/DraggableSticker';
|
import DraggableSticker, { StickerData } from '../components/DraggableSticker';
|
||||||
import StickerTray from '../components/StickerTray';
|
import StickerTray from '../components/StickerTray';
|
||||||
import FrameOverlay from '../components/FrameOverlay';
|
import FrameOverlay from '../components/FrameOverlay';
|
||||||
@@ -70,6 +71,13 @@ export default function PreviewScreen({
|
|||||||
const [selectedStickerId, setSelectedStickerId] = useState<string | null>(null);
|
const [selectedStickerId, setSelectedStickerId] = useState<string | null>(null);
|
||||||
const [showStickerTray, setShowStickerTray] = useState<boolean>(false);
|
const [showStickerTray, setShowStickerTray] = useState<boolean>(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
logger.log(`PreviewScreen mounted with ${photoUris.length} photos. currentPhoto: ${photoUris[photoUris.length - 1]}`);
|
||||||
|
return () => {
|
||||||
|
logger.log('PreviewScreen unmounting.');
|
||||||
|
};
|
||||||
|
}, [photoUris]);
|
||||||
|
|
||||||
// Idle Timer state
|
// Idle Timer state
|
||||||
const idleTimerRef = useRef<any>(null);
|
const idleTimerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
|
||||||
|
const logFileUri = FileSystem.documentDirectory + 'app_logs.txt';
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
log: async (message: string) => {
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const logLine = `[INFO] ${timestamp}: ${message}\n`;
|
||||||
|
console.log(logLine.trim());
|
||||||
|
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(logFileUri);
|
||||||
|
if (!fileInfo.exists) {
|
||||||
|
await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
} else {
|
||||||
|
// Read existing and append (simplistic approach for small logs)
|
||||||
|
// expo-file-system doesn't have an append method out of the box in older versions,
|
||||||
|
// but wait, we can just read, then write. To avoid OOM we should truncate if too big.
|
||||||
|
const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
// Keep last 100000 chars
|
||||||
|
let newContent = current + logLine;
|
||||||
|
if (newContent.length > 100000) {
|
||||||
|
newContent = newContent.slice(-100000);
|
||||||
|
}
|
||||||
|
await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to write log:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
error: async (message: string, error?: any) => {
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
let errorString = '';
|
||||||
|
if (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorString = `\n Name: ${error.name}\n Message: ${error.message}\n Stack: ${error.stack}`;
|
||||||
|
} else if (typeof error === 'object') {
|
||||||
|
errorString = `\n Object: ${JSON.stringify(error)}`;
|
||||||
|
} else {
|
||||||
|
errorString = `\n Value: ${String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logLine = `[ERROR] ${timestamp}: ${message}${errorString}\n`;
|
||||||
|
console.error(logLine.trim());
|
||||||
|
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(logFileUri);
|
||||||
|
if (!fileInfo.exists) {
|
||||||
|
await FileSystem.writeAsStringAsync(logFileUri, logLine, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
} else {
|
||||||
|
const current = await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
let newContent = current + logLine;
|
||||||
|
if (newContent.length > 100000) {
|
||||||
|
newContent = newContent.slice(-100000);
|
||||||
|
}
|
||||||
|
await FileSystem.writeAsStringAsync(logFileUri, newContent, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to write error log:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
readLogs: async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(logFileUri);
|
||||||
|
if (!fileInfo.exists) return 'Keine Logs vorhanden.';
|
||||||
|
return await FileSystem.readAsStringAsync(logFileUri, { encoding: FileSystem.EncodingType.UTF8 });
|
||||||
|
} catch (e) {
|
||||||
|
return `Fehler beim Lesen der Logs: ${e}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearLogs: async () => {
|
||||||
|
try {
|
||||||
|
await FileSystem.deleteAsync(logFileUri, { idempotent: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to clear logs:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user