update: implement event-based photo storage, add event name validation, version apks

This commit is contained in:
2026-06-12 18:45:21 +02:00
parent 652a3b8418
commit 8664c38908
8 changed files with 252 additions and 94 deletions
+85 -75
View File
@@ -337,81 +337,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
</View>
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faUserGear} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>System & Hardware</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Drucker IP-Adresse</Text>
<TextInput
style={styles.input}
placeholder="z.B. 192.168.1.100"
placeholderTextColor={THEME.colors.textMuted}
value={printerIp}
onChangeText={setPrinterIp}
keyboardType="numeric"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Admin-Passwort</Text>
<TextInput
style={styles.input}
placeholder="Standard: 1234"
placeholderTextColor={THEME.colors.textMuted}
secureTextEntry
maxLength={20}
value={password}
onChangeText={setPassword}
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} />
@@ -638,6 +563,91 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
)}
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faUserGear} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>System & Hardware</Text>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Drucker IP-Adresse</Text>
<TextInput
style={styles.input}
placeholder="z.B. 192.168.1.100"
placeholderTextColor={THEME.colors.textMuted}
value={printerIp}
onChangeText={setPrinterIp}
keyboardType="numeric"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Admin-Passwort</Text>
<TextInput
style={styles.input}
placeholder="Standard: 1234"
placeholderTextColor={THEME.colors.textMuted}
secureTextEntry
maxLength={20}
value={password}
onChangeText={setPassword}
keyboardType="number-pad"
/>
</View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Tablet-Frontkamera als Fallback erzwingen</Text>
<Switch
value={useFrontCamera}
onValueChange={setUseFrontCamera}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>App-Protokollierung (Logs) aktivieren</Text>
<Switch
value={enableLogs}
onValueChange={setEnableLogs}
trackColor={{ false: THEME.colors.surfaceSecondary, true: THEME.colors.accent }}
thumbColor={THEME.colors.text}
/>
</View>
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<FontAwesomeIcon icon={faLock} size={16} color={THEME.colors.accent} />
<Text style={styles.cardTitle}>Sicherheit & Kiosk-Modus</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}
+3 -1
View File
@@ -57,6 +57,7 @@ interface CameraScreenProps {
footerText: string;
useFrontCamera: boolean;
isObscured: boolean;
eventText: string;
}
export default function CameraScreen({
@@ -76,6 +77,7 @@ export default function CameraScreen({
footerText,
useFrontCamera,
isObscured,
eventText,
}: CameraScreenProps) {
const [permission, requestPermission] = useCameraPermissions();
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
@@ -354,7 +356,7 @@ export default function CameraScreen({
// Auto-save the high-res original raw photo to the gallery
try {
await saveToGallery(capturedUri);
await saveToGallery(capturedUri, 'Raw', eventText);
console.log('Auto-saved high-res raw capture to gallery');
} catch (e) {
console.error('Failed to auto-save raw capture:', e);
+3 -3
View File
@@ -289,7 +289,7 @@ export default function PreviewScreen({
});
setStatusMessage('Wird in Galerie gespeichert...');
await saveToGallery(printUri);
await saveToGallery(printUri, 'Collage', eventText);
if (!mountedRef.current) return;
setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉');
@@ -331,7 +331,7 @@ export default function PreviewScreen({
const capturedUri = await captureComposite(state.currentPhoto);
if (!mountedRef.current) return;
setStatusMessage('Wird gespeichert...');
await saveToGallery(capturedUri);
await saveToGallery(capturedUri, 'Collage', eventText);
if (!mountedRef.current) return;
setStatusMessage('Erfolgreich gespeichert!');
setTimeout(() => {
@@ -378,7 +378,7 @@ export default function PreviewScreen({
const capturedUri = await captureComposite(currentPhoto);
if (!mountedRef.current) return;
setStatusMessage('Wird vor dem Wiederholen gespeichert...');
await saveToGallery(capturedUri);
await saveToGallery(capturedUri, 'Collage', eventText);
if (!mountedRef.current) return;
} catch (e) {
if (!mountedRef.current) return;
+53 -12
View File
@@ -1,6 +1,5 @@
import { getPermissionsAsync, requestPermissionsAsync, createAssetAsync, getAlbumAsync, createAlbumAsync, addAssetsToAlbumAsync } from 'expo-media-library';
const ALBUM_NAME = 'Schnappix';
import * as FileSystem from 'expo-file-system';
/**
* Request necessary media library permissions.
@@ -20,44 +19,86 @@ export async function requestStoragePermission(): Promise<boolean> {
return false;
}
function getFormattedDateTime(): string {
const d = new Date();
const date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const time = `${String(d.getHours()).padStart(2, '0')}-${String(d.getMinutes()).padStart(2, '0')}-${String(d.getSeconds()).padStart(2, '0')}`;
return `${date}_${time}`;
}
/**
* Saves a local image file directly to the public DCIM gallery inside the "Schnappix" album.
* Saves a local image file directly to the public DCIM/Pictures gallery inside a structured album.
*
* @param localUri The local file URI of the image (temp cached file).
* @param type 'Raw' for original photos, 'Collage' for final prints.
* @param eventName The name of the event to use for folder and file naming.
* @returns Promise<string> The permanent URI of the saved asset in the gallery.
*/
export async function saveToGallery(localUri: string): Promise<string> {
export async function saveToGallery(localUri: string, type: 'Raw' | 'Collage', eventName: string): Promise<string> {
const hasPermission = await requestStoragePermission();
if (!hasPermission) {
throw new Error('Storage permission not granted. Cannot save photo to gallery.');
}
// 1. Create a media asset from the local file
const asset = await createAssetAsync(localUri);
// Clean event name for file system usage
const safeEventName = eventName.replace(/[^a-zA-Z0-9_-]/g, '').trim() || 'Event';
const timestamp = getFormattedDateTime();
const fileExtension = localUri.split('.').pop() || 'jpg';
// Format: EventName_YYYY-MM-DD_HH-mm-ss_Type.jpg
const newFileName = `${safeEventName}_${timestamp}_${type}.${fileExtension}`;
const newLocalUri = `${FileSystem.cacheDirectory}${newFileName}`;
// Copy the file to the new location to force the filename
await FileSystem.copyAsync({
from: localUri,
to: newLocalUri,
});
// 1. Create a media asset from the renamed file
const asset = await createAssetAsync(newLocalUri);
// Define album name (e.g. "Schnappix/EventName/Originale")
const subFolder = type === 'Raw' ? 'Originale' : 'Collagen';
const ALBUM_NAME = `Schnappix/${safeEventName}/${subFolder}`;
try {
// 2. Check if the "Schnappix" album already exists
// 2. Check if the album already exists
const album = await getAlbumAsync(ALBUM_NAME);
if (!album) {
// 3. If it doesn't exist, create it with our asset
// copyAsset=true completely bypasses the Android 11+ OS prompt!
await createAlbumAsync(ALBUM_NAME, asset, true);
console.log(`Created new album "${ALBUM_NAME}" and copied photo.`);
} else {
// 4. If it exists, add our asset to it
// copyAsset=true completely bypasses the Android 11+ OS prompt!
await addAssetsToAlbumAsync([asset], album, true);
console.log(`Copied photo to existing album "${ALBUM_NAME}".`);
}
return asset.uri;
} catch (e: any) {
console.error('Error saving asset to album, returning fallback asset URI:', e);
console.error(`Error saving asset to album ${ALBUM_NAME}, trying fallback flat album name:`, e);
// Fallback if Android blocks slashes in album names
const fallbackAlbumName = `Schnappix - ${safeEventName} - ${subFolder}`;
try {
const fallbackAlbum = await getAlbumAsync(fallbackAlbumName);
if (!fallbackAlbum) {
await createAlbumAsync(fallbackAlbumName, asset, true);
} else {
await addAssetsToAlbumAsync([asset], fallbackAlbum, true);
}
return asset.uri;
} catch {
return asset.id;
} catch (fallbackErr) {
console.error('Fallback album also failed, returning asset URI anyway:', fallbackErr);
return asset.uri;
}
} finally {
// Cleanup our temp renamed file
try {
await FileSystem.deleteAsync(newLocalUri, { idempotent: true });
} catch (e) {
// ignore
}
}
}