UI refactor: Admin Screen redesign, Timers, Flash timing, Date overlay
This commit is contained in:
@@ -184,7 +184,7 @@ export default function App() {
|
|||||||
initialFrameId={settings.selectedFrameId}
|
initialFrameId={settings.selectedFrameId}
|
||||||
availableFrameIds={settings.availableFrameIds}
|
availableFrameIds={settings.availableFrameIds}
|
||||||
dateOverlay={settings.dateOverlay}
|
dateOverlay={settings.dateOverlay}
|
||||||
dateOverlayPosition={settings.dateOverlayPosition}
|
dateOverlayTransform={settings.dateOverlayTransform}
|
||||||
eventText={settings.eventText}
|
eventText={settings.eventText}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"name": "Schnappix",
|
"name": "Schnappix",
|
||||||
"slug": "Schnappix",
|
"slug": "Schnappix",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"orientation": "landscape",
|
"orientation": "landscape",
|
||||||
"icon": "./Icon.png",
|
"icon": "./Icon.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "com.schnappix",
|
"package": "com.schnappix",
|
||||||
|
"versionCode": 2,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#050510",
|
"backgroundColor": "#050510",
|
||||||
"foregroundImage": "./Icon-padded.png"
|
"foregroundImage": "./Icon-padded.png"
|
||||||
|
|||||||
+257
-147
@@ -10,6 +10,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Modal,
|
Modal,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
|
PanResponder,
|
||||||
} 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';
|
||||||
@@ -54,13 +55,7 @@ const DATE_OVERLAY_OPTIONS: { value: DateOverlayMode; label: string }[] = [
|
|||||||
{ value: 'datetime', label: 'Datum + Uhrzeit' },
|
{ 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) {
|
export default function AdminScreen({ currentSettings, onSave, onClose }: AdminScreenProps) {
|
||||||
const [countdown, setCountdown] = useState<string>(String(currentSettings.countdownDuration));
|
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 [selectedFrameId, setSelectedFrameId] = useState<string | null>(currentSettings.selectedFrameId);
|
||||||
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
|
const [availableFrameIds, setAvailableFrameIds] = useState<string[]>(currentSettings.availableFrameIds || []);
|
||||||
const [dateOverlay, setDateOverlay] = useState<DateOverlayMode>(currentSettings.dateOverlay);
|
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 [eventText, setEventText] = useState<string>(currentSettings.eventText);
|
||||||
const [burstCount, setBurstCount] = useState<string>(String(currentSettings.burstCount));
|
const [burstCount, setBurstCount] = useState<number>(currentSettings.burstCount);
|
||||||
const [burstInterval, setBurstInterval] = useState<string>(String(currentSettings.burstIntervalSec));
|
const [burstIntervalSec, setBurstIntervalSec] = useState<number>(currentSettings.burstIntervalSec);
|
||||||
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
|
const [welcomeText, setWelcomeText] = useState<string>(currentSettings.welcomeText);
|
||||||
const [footerText, setFooterText] = useState<string>(currentSettings.footerText);
|
const [footerText, setFooterText] = useState<string>(currentSettings.footerText);
|
||||||
const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera);
|
const [useFrontCamera, setUseFrontCamera] = useState<boolean>(currentSettings.useFrontCamera);
|
||||||
const [enableLogs, setEnableLogs] = useState<boolean>(currentSettings.enableLogs);
|
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 = () => {
|
const hasUnsavedChanges = () => {
|
||||||
return (
|
return (
|
||||||
countdown !== String(currentSettings.countdownDuration) ||
|
countdown !== String(currentSettings.countdownDuration) ||
|
||||||
@@ -96,10 +114,10 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
selectedFrameId !== currentSettings.selectedFrameId ||
|
selectedFrameId !== currentSettings.selectedFrameId ||
|
||||||
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
|
availableFrameIds.join() !== (currentSettings.availableFrameIds || []).join() ||
|
||||||
dateOverlay !== currentSettings.dateOverlay ||
|
dateOverlay !== currentSettings.dateOverlay ||
|
||||||
dateOverlayPosition !== currentSettings.dateOverlayPosition ||
|
JSON.stringify(dateOverlayTransform) !== JSON.stringify(currentSettings.dateOverlayTransform) ||
|
||||||
eventText !== currentSettings.eventText ||
|
eventText !== currentSettings.eventText ||
|
||||||
burstCount !== String(currentSettings.burstCount) ||
|
burstCount !== currentSettings.burstCount ||
|
||||||
burstInterval !== String(currentSettings.burstIntervalSec) ||
|
burstIntervalSec !== currentSettings.burstIntervalSec ||
|
||||||
welcomeText !== currentSettings.welcomeText ||
|
welcomeText !== currentSettings.welcomeText ||
|
||||||
footerText !== currentSettings.footerText ||
|
footerText !== currentSettings.footerText ||
|
||||||
useFrontCamera !== currentSettings.useFrontCamera ||
|
useFrontCamera !== currentSettings.useFrontCamera ||
|
||||||
@@ -129,7 +147,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
return () => subscription.remove();
|
return () => subscription.remove();
|
||||||
}, [
|
}, [
|
||||||
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
|
countdown, printerIp, password, kioskActive, frameMode, selectedFrameId, availableFrameIds,
|
||||||
dateOverlay, dateOverlayPosition, eventText, burstCount, burstInterval, welcomeText,
|
dateOverlay, dateOverlayTransform, eventText, burstCount, burstIntervalSec, welcomeText,
|
||||||
footerText, useFrontCamera, enableLogs, currentSettings
|
footerText, useFrontCamera, enableLogs, currentSettings
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -206,17 +224,8 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const burst = parseInt(burstCount, 10);
|
const burst = burstCount;
|
||||||
if (isNaN(burst) || burst < 1 || burst > 5) {
|
const interval = burstIntervalSec;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) {
|
if (frameMode === 'always' && selectedFrameId === null && FRAMES.length > 0) {
|
||||||
Alert.alert('Hinweis', 'Bitte wähle einen Rahmen aus, wenn der Modus "Immer an" aktiviert ist.');
|
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,
|
selectedFrameId: frameMode === 'always' ? selectedFrameId : null,
|
||||||
availableFrameIds: frameMode === 'available' ? availableFrameIds : [],
|
availableFrameIds: frameMode === 'available' ? availableFrameIds : [],
|
||||||
dateOverlay,
|
dateOverlay,
|
||||||
dateOverlayPosition,
|
dateOverlayTransform,
|
||||||
eventText: eventText.trim(),
|
eventText: eventText.trim(),
|
||||||
burstCount: burst,
|
burstCount: burst,
|
||||||
burstIntervalSec: interval,
|
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 }[],
|
options: { value: T; label: string }[],
|
||||||
current: T,
|
current: T,
|
||||||
onChange: (v: T) => void,
|
onChange: (v: T) => void,
|
||||||
@@ -330,12 +339,9 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
<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={faUserGear} size={16} color={THEME.colors.accent} />
|
||||||
<Text style={styles.cardTitle}>Drucker-Konfiguration</Text>
|
<Text style={styles.cardTitle}>System & Hardware</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.cardDesc}>
|
|
||||||
Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers.
|
|
||||||
</Text>
|
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Drucker IP-Adresse</Text>
|
<Text style={styles.label}>Drucker IP-Adresse</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -347,24 +353,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
/>
|
/>
|
||||||
</View>
|
</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}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Admin-Passwort</Text>
|
<Text style={styles.label}>Admin-Passwort</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -378,6 +366,57 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
keyboardType="number-pad"
|
keyboardType="number-pad"
|
||||||
/>
|
/>
|
||||||
</View>
|
</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}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Begrüßungstext</Text>
|
<Text style={styles.label}>Begrüßungstext</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -399,7 +438,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.inputGroup}>
|
<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
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="z.B. Jannik's Party"
|
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.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faImages} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faCameraRetro} size={16} color={THEME.colors.accent} />
|
||||||
<Text style={styles.cardTitle}>Serienbild / Burst-Modus</Text>
|
<Text style={styles.cardTitle}>Foto-Ablauf</Text>
|
||||||
</View>
|
</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}>
|
<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>
|
</Text>
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Anzahl Fotos pro Durchgang (1–5)</Text>
|
<Text style={styles.label}>Anzahl Fotos pro Durchgang</Text>
|
||||||
<TextInput
|
{renderSegment(
|
||||||
style={styles.input}
|
[
|
||||||
placeholder="Standard: 1"
|
{ value: 1, label: '1 Bild' },
|
||||||
placeholderTextColor={THEME.colors.textMuted}
|
{ value: 2, label: '2 Bilder' },
|
||||||
value={burstCount}
|
{ value: 3, label: '3 Bilder' },
|
||||||
onChangeText={setBurstCount}
|
{ value: 4, label: '4 Bilder' },
|
||||||
keyboardType="number-pad"
|
{ value: 5, label: '5 Bilder' },
|
||||||
/>
|
],
|
||||||
|
burstCount,
|
||||||
|
setBurstCount
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden, 3–30)</Text>
|
<Text style={styles.label}>Abstand zwischen Fotos (Sekunden)</Text>
|
||||||
<TextInput
|
{renderSegment(
|
||||||
style={styles.input}
|
[
|
||||||
placeholder="Standard: 5"
|
{ value: 3, label: '3s' },
|
||||||
placeholderTextColor={THEME.colors.textMuted}
|
{ value: 5, label: '5s' },
|
||||||
value={burstInterval}
|
{ value: 10, label: '10s' },
|
||||||
onChangeText={setBurstInterval}
|
{ value: 15, label: '15s' },
|
||||||
keyboardType="number-pad"
|
{ value: 20, label: '20s' },
|
||||||
/>
|
],
|
||||||
</View>
|
burstIntervalSec,
|
||||||
<View style={styles.toggleRow}>
|
setBurstIntervalSec
|
||||||
<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>
|
</View>
|
||||||
|
|
||||||
<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={faImages} size={16} color={THEME.colors.accent} />
|
||||||
<Text style={styles.cardTitle}>Fotorahmen</Text>
|
<Text style={styles.cardTitle}>Gestaltung: Fotorahmen</Text>
|
||||||
</View>
|
</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)}
|
{renderSegment(FRAME_MODE_OPTIONS, frameMode, setFrameMode)}
|
||||||
|
|
||||||
{frameMode === 'always' && FRAMES.length > 0 && (
|
{frameMode === 'always' && FRAMES.length > 0 && (
|
||||||
@@ -528,72 +564,80 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
</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>
|
||||||
|
|
||||||
<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} />
|
||||||
<Text style={styles.cardTitle}>Datum/Uhrzeit-Einblendung</Text>
|
<Text style={styles.cardTitle}>Gestaltung: Datum/Uhrzeit-Einblendung</Text>
|
||||||
</View>
|
</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)}
|
{renderSegment(DATE_OVERLAY_OPTIONS, dateOverlay, setDateOverlay)}
|
||||||
|
|
||||||
{dateOverlay !== 'off' && (
|
{dateOverlay !== 'off' && (
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Position der Einblendung</Text>
|
<Text style={styles.label}>Vorschau: Positionieren & Drehen</Text>
|
||||||
{renderSegment(DATE_POSITION_OPTIONS, dateOverlayPosition, setDateOverlayPosition)}
|
<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>
|
</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}>
|
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={THEME.gradient.primary}
|
colors={THEME.gradient.primary}
|
||||||
@@ -878,4 +922,70 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||||
borderRadius: 8,
|
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(() => {
|
useEffect(() => {
|
||||||
if (countdown !== null) {
|
if (countdown !== null) {
|
||||||
scale.value = 0.5;
|
scale.value = 1;
|
||||||
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
|
scale.value = withSequence(
|
||||||
scale.value = withTiming(1.0, { duration: 150 });
|
withTiming(1.5, { duration: 100 }),
|
||||||
});
|
withSpring(1, { damping: 10, stiffness: 100 })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [countdown]);
|
}, [countdown]);
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ export default function CameraScreen({
|
|||||||
|
|
||||||
// Turn off white flash effect
|
// Turn off white flash effect
|
||||||
const turnFlashOff = useCallback(() => {
|
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)
|
// 1. Request built-in camera permission on mount (just in case we fallback)
|
||||||
@@ -319,7 +320,7 @@ export default function CameraScreen({
|
|||||||
turnFlashOn();
|
turnFlashOn();
|
||||||
|
|
||||||
// Wait for UI to render the flash before blocking main thread with capture
|
// 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;
|
let capturedUri: string;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ interface PreviewScreenProps {
|
|||||||
availableFrameIds: string[];
|
availableFrameIds: string[];
|
||||||
// Date overlay settings
|
// Date overlay settings
|
||||||
dateOverlay: 'off' | 'date' | 'datetime';
|
dateOverlay: 'off' | 'date' | 'datetime';
|
||||||
dateOverlayPosition: string;
|
dateOverlayTransform: { x: number; y: number; rotation: number; scale: number };
|
||||||
// Event text
|
// Event text
|
||||||
eventText: string;
|
eventText: string;
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ export default function PreviewScreen({
|
|||||||
initialFrameId,
|
initialFrameId,
|
||||||
availableFrameIds,
|
availableFrameIds,
|
||||||
dateOverlay,
|
dateOverlay,
|
||||||
dateOverlayPosition,
|
dateOverlayTransform,
|
||||||
eventText,
|
eventText,
|
||||||
}: PreviewScreenProps) {
|
}: PreviewScreenProps) {
|
||||||
const [layout, setLayout] = useState<CollageLayout>('single');
|
const [layout, setLayout] = useState<CollageLayout>('single');
|
||||||
@@ -176,19 +176,15 @@ export default function PreviewScreen({
|
|||||||
|
|
||||||
// Get date overlay position styles
|
// Get date overlay position styles
|
||||||
const getDatePositionStyle = () => {
|
const getDatePositionStyle = () => {
|
||||||
switch (dateOverlayPosition) {
|
return {
|
||||||
case 'bottom-left':
|
position: 'absolute' as const,
|
||||||
return { bottom: 12, left: 12 };
|
left: `${dateOverlayTransform.x}%` as any,
|
||||||
case 'top-right':
|
top: `${dateOverlayTransform.y}%` as any,
|
||||||
return { top: 12, right: 12 };
|
transform: [
|
||||||
case 'top-left':
|
{ rotate: `${dateOverlayTransform.rotation}deg` },
|
||||||
return { top: 12, left: 12 };
|
{ scale: dateOverlayTransform.scale }
|
||||||
case 'bottom-center':
|
]
|
||||||
return { bottom: 12, left: 0, right: 0, alignItems: 'center' as const };
|
};
|
||||||
case 'bottom-right':
|
|
||||||
default:
|
|
||||||
return { bottom: 12, right: 12 };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Sticker handlers ──
|
// ── 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);
|
const handleExitRef = useRef<(() => void) | null>(null);
|
||||||
handleExitRef.current = async () => {
|
handleExitRef.current = async () => {
|
||||||
@@ -389,7 +357,7 @@ export default function PreviewScreen({
|
|||||||
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
||||||
idleTimerRef.current = setTimeout(() => {
|
idleTimerRef.current = setTimeout(() => {
|
||||||
if (handleExitRef.current) handleExitRef.current();
|
if (handleExitRef.current) handleExitRef.current();
|
||||||
}, 60000);
|
}, 120000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -681,10 +649,7 @@ export default function PreviewScreen({
|
|||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</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 && (
|
{photoUris.length < 4 && (
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}>
|
<TouchableOpacity style={[styles.actionBtn, styles.addBtn, isProcessing && styles.btnDisabled]} onPress={onAddAnother} disabled={isProcessing}>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface AppSettings {
|
|||||||
availableFrameIds: string[];
|
availableFrameIds: string[];
|
||||||
// Date/Time overlay
|
// Date/Time overlay
|
||||||
dateOverlay: 'off' | 'date' | 'datetime';
|
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)
|
// Event text (shown in date sticker & welcome)
|
||||||
eventText: string;
|
eventText: string;
|
||||||
// Continuous shooting / burst
|
// Continuous shooting / burst
|
||||||
@@ -42,7 +42,7 @@ const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
selectedFrameId: null,
|
selectedFrameId: null,
|
||||||
availableFrameIds: FRAMES.map(f => f.id),
|
availableFrameIds: FRAMES.map(f => f.id),
|
||||||
dateOverlay: 'off',
|
dateOverlay: 'off',
|
||||||
dateOverlayPosition: 'bottom-right',
|
dateOverlayTransform: { x: 0, y: 0, rotation: 0, scale: 1 },
|
||||||
eventText: '',
|
eventText: '',
|
||||||
burstCount: 1,
|
burstCount: 1,
|
||||||
burstIntervalSec: 5,
|
burstIntervalSec: 5,
|
||||||
|
|||||||
Reference in New Issue
Block a user