UI refactor: Admin Screen redesign, Timers, Flash timing, Date overlay
This commit is contained in:
+257
-147
@@ -10,6 +10,7 @@ import {
|
||||
Alert,
|
||||
Modal,
|
||||
BackHandler,
|
||||
PanResponder,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
@@ -54,13 +55,7 @@ const DATE_OVERLAY_OPTIONS: { value: DateOverlayMode; label: string }[] = [
|
||||
{ value: 'datetime', label: 'Datum + Uhrzeit' },
|
||||
];
|
||||
|
||||
const DATE_POSITION_OPTIONS: { value: DatePosition; label: string }[] = [
|
||||
{ value: 'bottom-right', label: 'Unten rechts' },
|
||||
{ value: 'bottom-left', label: 'Unten links' },
|
||||
{ value: 'bottom-center', label: 'Unten Mitte' },
|
||||
{ value: 'top-right', label: 'Oben rechts' },
|
||||
{ value: 'top-left', label: 'Oben links' },
|
||||
];
|
||||
|
||||
|
||||
export default function AdminScreen({ currentSettings, onSave, onClose }: AdminScreenProps) {
|
||||
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
|
||||
@@ -77,15 +72,38 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
|
||||
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
|
||||
const [dateOverlay, setDateOverlay] = useState<DateOverlayMode>(currentSettings.dateOverlay);
|
||||
const [dateOverlayPosition, setDateOverlayPosition] = useState<DatePosition>(currentSettings.dateOverlayPosition);
|
||||
const [dateOverlayTransform, setDateOverlayTransform] = useState(currentSettings.dateOverlayTransform);
|
||||
const [eventText, setEventText] = useState<string>(currentSettings.eventText);
|
||||
const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount));
|
||||
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
|
||||
const [burstCount, setBurstCount] = useState<number>(currentSettings.burstCount);
|
||||
const [burstIntervalSec, setBurstIntervalSec] = useState<number>(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 transformRef = useRef(dateOverlayTransform);
|
||||
useEffect(() => {
|
||||
transformRef.current = dateOverlayTransform;
|
||||
}, [dateOverlayTransform]);
|
||||
|
||||
const initialTransform = useRef(dateOverlayTransform);
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
initialTransform.current = transformRef.current;
|
||||
},
|
||||
onPanResponderMove: (e, gesture) => {
|
||||
setDateOverlayTransform({
|
||||
...transformRef.current,
|
||||
x: Math.max(0, Math.min(100, initialTransform.current.x + (gesture.dx / 3))),
|
||||
y: Math.max(0, Math.min(100, initialTransform.current.y + (gesture.dy / 2)))
|
||||
});
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
const hasUnsavedChanges = () => {
|
||||
return (
|
||||
countdown !== String(currentSettings.countdownDuration) ||
|
||||
@@ -96,10 +114,10 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
selectedFrameId !== currentSettings.selectedFrameId ||
|
||||
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
|
||||
dateOverlay !== currentSettings.dateOverlay ||
|
||||
dateOverlayPosition !== currentSettings.dateOverlayPosition ||
|
||||
JSON.stringify(dateOverlayTransform) !== JSON.stringify(currentSettings.dateOverlayTransform) ||
|
||||
eventText !== currentSettings.eventText ||
|
||||
burstCount !== String(currentSettings.burstCount) ||
|
||||
burstInterval !== String(currentSettings.burstIntervalSec) ||
|
||||
burstCount !== currentSettings.burstCount ||
|
||||
burstIntervalSec !== currentSettings.burstIntervalSec ||
|
||||
welcomeText !== currentSettings.welcomeText ||
|
||||
footerText !== currentSettings.footerText ||
|
||||
useFrontCamera !== currentSettings.useFrontCamera ||
|
||||
@@ -129,7 +147,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
return () => subscription.remove();
|
||||
}, [
|
||||
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
|
||||
dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText,
|
||||
dateOverlay, dateOverlayTransform, eventText, burstCount, burstIntervalSec, welcomeText,
|
||||
footerText, useFrontCamera, enableLogs, currentSettings
|
||||
]);
|
||||
|
||||
@@ -206,17 +224,8 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
return;
|
||||
}
|
||||
|
||||
const burst = parseInt(burstCount, 10);
|
||||
if (isNaN(burst) || burst < 1 || burst > 5) {
|
||||
Alert.alert('Ungültige Eingabe', 'Die Serienbildanzahl muss zwischen 1 und 5 liegen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = parseInt(burstInterval, 10);
|
||||
if (isNaN(interval) || interval < 3 || interval > 30) {
|
||||
Alert.alert('Ungültige Eingabe', 'Der Abstand zwischen Serienbildern muss zwischen 3 und 30 Sekunden liegen.');
|
||||
return;
|
||||
}
|
||||
const burst = burstCount;
|
||||
const interval = burstIntervalSec;
|
||||
|
||||
if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) {
|
||||
Alert.alert('Hinweis', 'Bitte wähle einen Rahmen aus, wenn der Modus "Immer an" aktiviert ist.');
|
||||
@@ -238,7 +247,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
selectedFrameId: frameMode === 'always' ? selectedFrameId : null,
|
||||
availableFrameIds: frameMode === 'available' ? availableFrameIds : [],
|
||||
dateOverlay,
|
||||
dateOverlayPosition,
|
||||
dateOverlayTransform,
|
||||
eventText: eventText.trim(),
|
||||
burstCount: burst,
|
||||
burstIntervalSec: interval,
|
||||
@@ -257,7 +266,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
}
|
||||
};
|
||||
|
||||
const renderSegment = <T extends string>(
|
||||
const renderSegment = <T extends string | number>(
|
||||
options: { value: T; label: string }[],
|
||||
current: T,
|
||||
onChange: (v: T) => void,
|
||||
@@ -330,12 +339,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faPrint} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Drucker-Konfiguration</Text>
|
||||
<FontAwesomeIcon icon={faUserGear} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>System & Hardware</Text>
|
||||
</View>
|
||||
<Text style={styles.cardDesc}>
|
||||
Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers.
|
||||
</Text>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Drucker IP-Adresse</Text>
|
||||
<TextInput
|
||||
@@ -347,24 +353,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Fotobox-Verhalten</Text>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Countdown-Dauer (Sekunden)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Standard: 3"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={countdown}
|
||||
onChangeText={setCountdown}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Admin-Passwort</Text>
|
||||
<TextInput
|
||||
@@ -378,6 +366,57 @@ 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 style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>Kiosk-Modus Status:</Text>
|
||||
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}>
|
||||
{kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>Device-Owner-Modus:</Text>
|
||||
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}>
|
||||
{isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'}
|
||||
</Text>
|
||||
</View>
|
||||
{!isDeviceOwner && (
|
||||
<Text style={styles.kioskWarningText}>
|
||||
Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner.
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Bildschirm sperren (Kiosk-Modus)</Text>
|
||||
<Switch
|
||||
value={kioskActive}
|
||||
onValueChange={handleKioskToggle}
|
||||
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
|
||||
thumbColor={THEME.colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Startbildschirm</Text>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Begrüßungstext</Text>
|
||||
<TextInput
|
||||
@@ -399,7 +438,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Event-Name (für Datum-Sticker)</Text>
|
||||
<Text style={styles.label}>Event-Name (wird auch im Datum-Sticker & Collage verwendet)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="z.B. Jannik's Party"
|
||||
@@ -412,62 +451,59 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Serienbild / Burst-Modus</Text>
|
||||
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Foto-Ablauf</Text>
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Countdown-Dauer (Sekunden)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Standard: 3"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={countdown}
|
||||
onChangeText={setCountdown}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.label}>Serienbild</Text>
|
||||
<Text style={styles.cardDesc}>
|
||||
Mehrere Fotos automatisch hintereinander aufnehmen. Bei Anzahl = 1 wird nur ein einzelnes Foto aufgenommen.
|
||||
Mehrere Fotos automatisch hintereinander aufnehmen. Bei 1 Bild wird nur ein einzelnes Foto aufgenommen.
|
||||
</Text>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Anzahl Fotos pro Durchgang (1–5)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Standard: 1"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={burstCount}
|
||||
onChangeText={setBurstCount}
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
<Text style={styles.label}>Anzahl Fotos pro Durchgang</Text>
|
||||
{renderSegment(
|
||||
[
|
||||
{ value: 1, label: '1 Bild' },
|
||||
{ value: 2, label: '2 Bilder' },
|
||||
{ value: 3, label: '3 Bilder' },
|
||||
{ value: 4, label: '4 Bilder' },
|
||||
{ value: 5, label: '5 Bilder' },
|
||||
],
|
||||
burstCount,
|
||||
setBurstCount
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden, 3–30)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Standard: 5"
|
||||
placeholderTextColor={THEME.colors.textMuted}
|
||||
value={burstInterval}
|
||||
onChangeText={setBurstInterval}
|
||||
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}
|
||||
/>
|
||||
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden)</Text>
|
||||
{renderSegment(
|
||||
[
|
||||
{ value: 3, label: '3s' },
|
||||
{ value: 5, label: '5s' },
|
||||
{ value: 10, label: '10s' },
|
||||
{ value: 15, label: '15s' },
|
||||
{ value: 20, label: '20s' },
|
||||
],
|
||||
burstIntervalSec,
|
||||
setBurstIntervalSec
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Fotorahmen</Text>
|
||||
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Gestaltung: Fotorahmen</Text>
|
||||
</View>
|
||||
<Text style={styles.cardDesc}>
|
||||
Rahmen über die Fotos legen. "Aus" deaktiviert Rahmen, "Immer an" erzwingt einen gewählten Rahmen, "Verfügbar" lässt Gäste einen Rahmen auswählen.
|
||||
</Text>
|
||||
{renderSegment(FRAME_MODE_OPTIONS, frameMode, setFrameMode)}
|
||||
|
||||
{frameMode === 'always' && FRAMES.length > 0 && (
|
||||
@@ -528,72 +564,80 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{frameMode === 'always' && FRAMES.length === 0 && (
|
||||
<Text style={styles.warningText}>
|
||||
Noch keine Rahmen hinterlegt. Lege PNG-Dateien in assets/frames/ ab.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faCalendarDay} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Datum/Uhrzeit-Einblendung</Text>
|
||||
<Text style={styles.cardTitle}>Gestaltung: Datum/Uhrzeit-Einblendung</Text>
|
||||
</View>
|
||||
<Text style={styles.cardDesc}>
|
||||
Datum automatisch auf jedes Foto einblenden. Unabhängig davon ist der Datum-Sticker in der Bearbeitung immer verfügbar.
|
||||
</Text>
|
||||
{renderSegment(DATE_OVERLAY_OPTIONS, dateOverlay, setDateOverlay)}
|
||||
|
||||
{dateOverlay !== 'off' && (
|
||||
<View style={styles.inputGroup}>
|
||||
<Text style={styles.label}>Position der Einblendung</Text>
|
||||
{renderSegment(DATE_POSITION_OPTIONS, dateOverlayPosition, setDateOverlayPosition)}
|
||||
<Text style={styles.label}>Vorschau: Positionieren & Drehen</Text>
|
||||
<Text style={styles.cardDesc}>
|
||||
Ziehe den Text in der Vorschau mit dem Finger an die gewünschte Position. Passe Rotation und Größe mit den +/- Tasten an.
|
||||
</Text>
|
||||
|
||||
<View style={styles.previewBoxContainer}>
|
||||
<View style={styles.previewBox}>
|
||||
<View
|
||||
style={[
|
||||
styles.previewTextWrapper,
|
||||
{
|
||||
left: `${dateOverlayTransform.x}%`,
|
||||
top: `${dateOverlayTransform.y}%`,
|
||||
transform: [
|
||||
{ rotate: `${dateOverlayTransform.rotation}deg` },
|
||||
{ scale: dateOverlayTransform.scale }
|
||||
]
|
||||
}
|
||||
]}
|
||||
{...panResponder.panHandlers}
|
||||
>
|
||||
<Text style={styles.previewTextContent}>
|
||||
{dateOverlay === 'date' ? '01.01.2026' : '01.01.2026 14:00'}
|
||||
</Text>
|
||||
{eventText.length > 0 && (
|
||||
<Text style={styles.previewTextEvent}>{eventText}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.controlRow}>
|
||||
<View style={styles.controlColumn}>
|
||||
<Text style={styles.label}>Rotation</Text>
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, rotation: p.rotation - 5 }))}>
|
||||
<Text style={styles.controlBtnText}>-</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.controlValueText}>{dateOverlayTransform.rotation}°</Text>
|
||||
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, rotation: p.rotation + 5 }))}>
|
||||
<Text style={styles.controlBtnText}>+</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.controlColumn}>
|
||||
<Text style={styles.label}>Größe</Text>
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, scale: Math.max(0.5, p.scale - 0.1) }))}>
|
||||
<Text style={styles.controlBtnText}>-</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.controlValueText}>{dateOverlayTransform.scale.toFixed(1)}</Text>
|
||||
<TouchableOpacity style={styles.controlBtn} onPress={() => setDateOverlayTransform(p => ({ ...p, scale: Math.min(3.0, p.scale + 0.1) }))}>
|
||||
<Text style={styles.controlBtnText}>+</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
|
||||
<Text style={styles.cardTitle}>Kiosk-Modus & Sicherheit</Text>
|
||||
</View>
|
||||
<Text style={styles.cardDesc}>
|
||||
Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen.
|
||||
</Text>
|
||||
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>Kiosk-Modus Status:</Text>
|
||||
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}>
|
||||
{kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusRow}>
|
||||
<Text style={styles.statusLabel}>Device-Owner-Modus:</Text>
|
||||
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}>
|
||||
{isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!isDeviceOwner && (
|
||||
<Text style={styles.kioskWarningText}>
|
||||
Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Bildschirm sperren (Kiosk-Modus)</Text>
|
||||
<Switch
|
||||
value={kioskActive}
|
||||
onValueChange={handleKioskToggle}
|
||||
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
|
||||
thumbColor={THEME.colors.text}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
|
||||
<LinearGradient
|
||||
colors={THEME.gradient.primary}
|
||||
@@ -878,4 +922,70 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
borderRadius: 8,
|
||||
},
|
||||
previewBoxContainer: {
|
||||
alignItems: 'center',
|
||||
marginVertical: THEME.spacing.md,
|
||||
},
|
||||
previewBox: {
|
||||
width: 300,
|
||||
height: 200,
|
||||
backgroundColor: '#333',
|
||||
borderWidth: 2,
|
||||
borderColor: THEME.colors.border,
|
||||
borderRadius: THEME.borderRadius.md,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
previewTextWrapper: {
|
||||
position: 'absolute',
|
||||
padding: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderRadius: 8,
|
||||
},
|
||||
previewTextContent: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
previewTextEvent: {
|
||||
color: '#aaa',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
marginTop: 2,
|
||||
},
|
||||
controlRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
marginTop: THEME.spacing.md,
|
||||
},
|
||||
controlColumn: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
controlBtn: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
backgroundColor: THEME.colors.surfaceSecondary,
|
||||
borderWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
controlBtnText: {
|
||||
color: THEME.colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
controlValueText: {
|
||||
color: THEME.colors.text,
|
||||
fontSize: 16,
|
||||
width: 40,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,10 +111,11 @@ export default function CameraScreen({
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown !== null) {
|
||||
scale.value = 0.5;
|
||||
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
|
||||
scale.value = withTiming(1.0, { duration: 150 });
|
||||
});
|
||||
scale.value = 1;
|
||||
scale.value = withSequence(
|
||||
withTiming(1.5, { duration: 100 }),
|
||||
withSpring(1, { damping: 10, stiffness: 100 })
|
||||
);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
@@ -133,7 +134,7 @@ export default function CameraScreen({
|
||||
|
||||
// Turn off white flash effect
|
||||
const turnFlashOff = useCallback(() => {
|
||||
flashOpacity.value = withTiming(0, { duration: 400 });
|
||||
flashOpacity.value = withTiming(0, { duration: 800 });
|
||||
}, []);
|
||||
|
||||
// 1. Request built-in camera permission on mount (just in case we fallback)
|
||||
@@ -319,7 +320,7 @@ export default function CameraScreen({
|
||||
turnFlashOn();
|
||||
|
||||
// Wait for UI to render the flash before blocking main thread with capture
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
let capturedUri: string;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ interface PreviewScreenProps {
|
||||
availableFrameIds: string[];
|
||||
// Date overlay settings
|
||||
dateOverlay: 'off' | 'date' | 'datetime';
|
||||
dateOverlayPosition: string;
|
||||
dateOverlayTransform: { x: number; y: number; rotation: number; scale: number };
|
||||
// Event text
|
||||
eventText: string;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export default function PreviewScreen({
|
||||
initialFrameId,
|
||||
availableFrameIds,
|
||||
dateOverlay,
|
||||
dateOverlayPosition,
|
||||
dateOverlayTransform,
|
||||
eventText,
|
||||
}: PreviewScreenProps) {
|
||||
const [layout, setLayout] = useState<CollageLayout>('single');
|
||||
@@ -176,19 +176,15 @@ export default function PreviewScreen({
|
||||
|
||||
// Get date overlay position styles
|
||||
const getDatePositionStyle = () => {
|
||||
switch (dateOverlayPosition) {
|
||||
case 'bottom-left':
|
||||
return { bottom: 12, left: 12 };
|
||||
case 'top-right':
|
||||
return { top: 12, right: 12 };
|
||||
case 'top-left':
|
||||
return { top: 12, left: 12 };
|
||||
case 'bottom-center':
|
||||
return { bottom: 12, left: 0, right: 0, alignItems: 'center' as const };
|
||||
case 'bottom-right':
|
||||
default:
|
||||
return { bottom: 12, right: 12 };
|
||||
}
|
||||
return {
|
||||
position: 'absolute' as const,
|
||||
left: `${dateOverlayTransform.x}%` as any,
|
||||
top: `${dateOverlayTransform.y}%` as any,
|
||||
transform: [
|
||||
{ rotate: `${dateOverlayTransform.rotation}deg` },
|
||||
{ scale: dateOverlayTransform.scale }
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
// ── Sticker handlers ──
|
||||
@@ -310,35 +306,7 @@ export default function PreviewScreen({
|
||||
}
|
||||
};
|
||||
|
||||
// Saves to gallery without printing
|
||||
const handleSaveOnly = async () => {
|
||||
logger.log('Action: Clicked Save button');
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
if (!mountedRef.current) return;
|
||||
if (isProcessing) return;
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const saveUri = await captureComposite(currentPhoto);
|
||||
if (!mountedRef.current) return;
|
||||
setStatusMessage('Bild wird generiert...');
|
||||
|
||||
setStatusMessage('Wird in Galerie gespeichert...');
|
||||
await saveToGallery(saveUri);
|
||||
if (!mountedRef.current) return;
|
||||
|
||||
setStatusMessage('Erfolgreich gespeichert!');
|
||||
setTimeout(() => {
|
||||
if (!mountedRef.current) return;
|
||||
setIsProcessing(false);
|
||||
resetIdleTimer();
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
if (!mountedRef.current) return;
|
||||
console.error('Saving failed:', error);
|
||||
alert('Fehler: ' + error.message);
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExitRef = useRef<(() => void) | null>(null);
|
||||
handleExitRef.current = async () => {
|
||||
@@ -389,7 +357,7 @@ export default function PreviewScreen({
|
||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||
idleTimerRef.current = setTimeout(() => {
|
||||
if (handleExitRef.current) handleExitRef.current();
|
||||
}, 60000);
|
||||
}, 120000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -681,10 +649,7 @@ export default function PreviewScreen({
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
<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 }} />
|
||||
<Text style={[styles.actionBtnText, { color: THEME.colors.accent }]}>Nur auf Tablet speichern</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
{photoUris.length < 4 && (
|
||||
<TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}>
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface AppSettings {
|
||||
availableFrameIds: string[];
|
||||
// Date/Time overlay
|
||||
dateOverlay: 'off' | 'date' | 'datetime';
|
||||
dateOverlayPosition: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-center';
|
||||
dateOverlayTransform: { x: number; y: number; rotation: number; scale: number };
|
||||
// Event text (shown in date sticker & welcome)
|
||||
eventText: string;
|
||||
// Continuous shooting / burst
|
||||
@@ -42,7 +42,7 @@ const DEFAULT_SETTINGS: AppSettings = {
|
||||
selectedFrameId: null,
|
||||
availableFrameIds: FRAMES.map(f => f.id),
|
||||
dateOverlay: 'off',
|
||||
dateOverlayPosition: 'bottom-right',
|
||||
dateOverlayTransform: { x: 0, y: 0, rotation: 0, scale: 1 },
|
||||
eventText: '',
|
||||
burstCount: 1,
|
||||
burstIntervalSec: 5,
|
||||
|
||||
Reference in New Issue
Block a user