Initialize Schnappix Photo Booth application with custom UVC camera, silent Wi-Fi IPP printing, local gallery storage, and kiosk screen pinning support
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { THEME } from '../styles/theme';
|
||||
import KioskMode from '../../modules/kiosk-mode';
|
||||
import { AppSettings, saveSettings } from '../services/settings';
|
||||
|
||||
interface AdminScreenProps {
|
||||
currentSettings: AppSettings;
|
||||
onSave: (settings: AppSettings) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function AdminScreen({ currentSettings, onSave, onClose }: AdminScreenProps) {
|
||||
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
|
||||
const [printerIp, setPrinterIp] = useState<string>(currentSettings.printerIp);
|
||||
const [password, setPassword] = useState<string>(currentSettings.adminPassword);
|
||||
|
||||
const [kioskActive, setKioskActive] = useState<boolean>(false);
|
||||
const [isDeviceOwner, setIsDeviceOwner] = useState<boolean>(false);
|
||||
|
||||
// Load Kiosk and Device Owner status on mount
|
||||
useEffect(() => {
|
||||
const checkKioskStatus = () => {
|
||||
try {
|
||||
const active = KioskMode.isKioskActive();
|
||||
const owner = KioskMode.isDeviceOwner();
|
||||
setKioskActive(active);
|
||||
setIsDeviceOwner(owner);
|
||||
} catch (e) {
|
||||
console.warn('Failed to query native KioskMode module:', e);
|
||||
}
|
||||
};
|
||||
|
||||
checkKioskStatus();
|
||||
// Poll status every second
|
||||
const interval = setInterval(checkKioskStatus, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleKioskToggle = (enable: boolean) => {
|
||||
try {
|
||||
if (enable) {
|
||||
const success = KioskMode.startKiosk();
|
||||
if (success) {
|
||||
setKioskActive(true);
|
||||
Alert.alert(
|
||||
'Kiosk Mode Started',
|
||||
isDeviceOwner
|
||||
? 'True Kiosk Mode active. System navigation buttons are completely locked.'
|
||||
: 'Screen Pinning initiated. Accept the OS prompt to lock the screen.'
|
||||
);
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to start Kiosk Mode.');
|
||||
}
|
||||
} else {
|
||||
const success = KioskMode.stopKiosk();
|
||||
if (success) {
|
||||
setKioskActive(false);
|
||||
Alert.alert('Kiosk Mode Stopped', 'System navigation buttons are unlocked.');
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to stop Kiosk Mode.');
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
Alert.alert('Native Error', e.message || 'Kiosk module error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const duration = parseInt(countdown, 10);
|
||||
if (isNaN(duration) || duration < 1 || duration > 30) {
|
||||
Alert.alert('Invalid Input', 'Countdown duration must be a number between 1 and 30 seconds.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!printerIp.trim()) {
|
||||
Alert.alert('Invalid Input', 'Printer IP address cannot be empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim() || password.length < 4) {
|
||||
Alert.alert('Invalid Input', 'Admin password must be at least 4 digits.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated: AppSettings = {
|
||||
countdownDuration: duration,
|
||||
printerIp: printerIp.trim(),
|
||||
adminPassword: password.trim(),
|
||||
kioskModeEnabled: kioskActive,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveSettings(updated);
|
||||
onSave(updated);
|
||||
Alert.alert('Success', 'Settings saved successfully!');
|
||||
} catch (e) {
|
||||
Alert.alert('Error', 'Failed to save settings to disk.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.title}>SCHNAPPIX SETTINGS</Text>
|
||||
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
|
||||
<Text style={styles.closeBtnText}>Back to Booth</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
{/* Printer Configurations */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Printer Configuration</Text>
|
||||
<Text style={styles.cardDesc}>
|
||||
Configure the local IP address of your Wi-Fi connected Canon CP1300 printer.
|
||||
</Text>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Printer IP Address</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="e.g. 192.168.1.100"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={printerIp}
|
||||
onChangeText={setPrinterIp}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Booth Behavior */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Photo Booth Behavior</Text>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Countdown Duration (seconds)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Default: 3"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={countdown}
|
||||
onChangeText={setCountdown}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Admin Panel Password</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Default: 1234"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Security & Kiosk Mode */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Kiosk Mode & Security</Text>
|
||||
<Text style={styles.cardDesc}>
|
||||
Lock the screen to prevent guests from closing the app or opening system settings.
|
||||
</Text>
|
||||
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>Kiosk Mode State:</Text>
|
||||
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}>
|
||||
{kioskActive ? 'LOCKED 🔒' : 'UNLOCKED 🔓'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>Device Owner Mode:</Text>
|
||||
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}>
|
||||
{isDeviceOwner ? 'ACTIVE (True Lock) ✅' : 'INACTIVE (Screen Pinning Fallback) ⚠️'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!isDeviceOwner && (
|
||||
<Text style={styles.kioskWarningText}>
|
||||
Note: To enable True Lock task mode without prompt bypasses, make the app device owner using ADB.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Lock Screen (Kiosk Mode)</Text>
|
||||
<Switch
|
||||
value={kioskActive}
|
||||
onValueChange={handleKioskToggle}
|
||||
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.primary }}
|
||||
thumbColor={THEME.colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.saveBtn} onPress={handleSave}>
|
||||
<Text style={styles.saveBtnText}>SAVE SETTINGS</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: THEME.colors.background,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: THEME.spacing.lg,
|
||||
paddingHorizontal: THEME.spacing.xl,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
backgroundColor: THEME.colors.surface,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: '900',
|
||||
color: THEME.colors.text,
|
||||
letterSpacing: 2,
|
||||
},
|
||||
closeBtn: {
|
||||
backgroundColor: THEME.colors.surfaceSecondary,
|
||||
borderWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
paddingVertical: THEME.spacing.sm,
|
||||
paddingHorizontal: THEME.spacing.lg,
|
||||
borderRadius: THEME.borderRadius.round,
|
||||
},
|
||||
closeBtnText: {
|
||||
color: THEME.colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
scrollContent: {
|
||||
padding: THEME.spacing.xl,
|
||||
alignItems: 'center',
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
maxWidth: 680,
|
||||
backgroundColor: THEME.colors.surface,
|
||||
borderRadius: THEME.borderRadius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
padding: THEME.spacing.lg,
|
||||
marginBottom: THEME.spacing.lg,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: THEME.colors.text,
|
||||
marginBottom: THEME.spacing.xs,
|
||||
},
|
||||
cardDesc: {
|
||||
fontSize: 14,
|
||||
color: THEME.colors.textMuted,
|
||||
marginBottom: THEME.spacing.md,
|
||||
},
|
||||
inputGroup: {
|
||||
marginBottom: THEME.spacing.md,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
color: THEME.colors.text,
|
||||
marginBottom: THEME.spacing.xs,
|
||||
},
|
||||
input: {
|
||||
height: 48,
|
||||
backgroundColor: THEME.colors.surfaceSecondary,
|
||||
borderWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
borderRadius: THEME.borderRadius.sm,
|
||||
color: THEME.colors.text,
|
||||
paddingHorizontal: THEME.spacing.md,
|
||||
fontSize: 16,
|
||||
},
|
||||
statusRow: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: THEME.spacing.xs,
|
||||
},
|
||||
statusLabel: {
|
||||
fontSize: 14,
|
||||
color: THEME.colors.textMuted,
|
||||
marginRight: THEME.spacing.sm,
|
||||
},
|
||||
statusValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
statusActive: {
|
||||
color: THEME.colors.success,
|
||||
},
|
||||
statusInactive: {
|
||||
color: THEME.colors.textMuted,
|
||||
},
|
||||
statusWarning: {
|
||||
color: THEME.colors.accent,
|
||||
},
|
||||
kioskWarningText: {
|
||||
fontSize: 12,
|
||||
color: THEME.colors.textMuted,
|
||||
marginTop: THEME.spacing.xs,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
toggleRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: THEME.spacing.lg,
|
||||
paddingTop: THEME.spacing.md,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: THEME.colors.border,
|
||||
},
|
||||
toggleLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: THEME.colors.text,
|
||||
},
|
||||
saveBtn: {
|
||||
width: '100%',
|
||||
maxWidth: 680,
|
||||
height: 52,
|
||||
backgroundColor: THEME.colors.primary,
|
||||
borderRadius: THEME.borderRadius.md,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: THEME.spacing.sm,
|
||||
marginBottom: THEME.spacing.xl,
|
||||
},
|
||||
saveBtnText: {
|
||||
color: THEME.colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: 2,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user