diff --git a/App.tsx b/App.tsx index 19daf74d..249b9e55 100644 --- a/App.tsx +++ b/App.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { StyleSheet, View, SafeAreaView, StatusBar, ActivityIndicator } from 'react-native'; +import { StyleSheet, View, SafeAreaView, StatusBar, ActivityIndicator, Modal, TextInput, Text, TouchableOpacity } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import CameraScreen from './src/screens/CameraScreen'; import PreviewScreen from './src/screens/PreviewScreen'; @@ -27,6 +27,8 @@ export default function App() { const [photoUris, setPhotoUris] = useState([]); const [captureHistory, setCaptureHistory] = useState([]); const [settings, setSettings] = useState(null); + const [showEventModal, setShowEventModal] = useState(false); + const [tempEventName, setTempEventName] = useState(''); // 1. Load configuration settings on app start useEffect(() => { @@ -58,6 +60,25 @@ export default function App() { // 2. Navigation Actions const handleStartBooth = () => { + if (!settings?.eventText || settings.eventText.trim() === '') { + setTempEventName(''); + setShowEventModal(true); + return; + } + setPhotoUris([]); + setCaptureHistory([]); + setCurrentScreen('camera'); + }; + + const handleSaveEventName = async () => { + if (!settings) return; + const newName = tempEventName.trim() || 'Event'; + const updated = { ...settings, eventText: newName }; + await saveSettings(updated); + setSettings(updated); + setShowEventModal(false); + + // Resume start setPhotoUris([]); setCaptureHistory([]); setCurrentScreen('camera'); @@ -168,6 +189,7 @@ export default function App() { selectedFrameId={settings.selectedFrameId} footerText={settings.footerText} useFrontCamera={settings.useFrontCamera} + eventText={settings.eventText} /> @@ -200,6 +222,34 @@ export default function App() { /> )} + + {/* Event Name Modal */} + + + + Kein Event-Name gefunden + + Möchtest du jetzt einen festlegen? (Ansonsten kann kein Foto gemacht werden). + + + + setShowEventModal(false)}> + Abbrechen + + + Speichern & Starten + + + + + ); @@ -216,4 +266,56 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.8)', + justifyContent: 'center', + alignItems: 'center', + }, + modalContent: { + backgroundColor: '#1a1a24', + padding: 30, + borderRadius: 16, + width: '80%', + maxWidth: 500, + borderWidth: 1, + borderColor: '#333', + }, + modalTitle: { + color: '#fff', + fontSize: 24, + fontWeight: 'bold', + marginBottom: 10, + }, + modalText: { + color: '#aaa', + fontSize: 16, + marginBottom: 20, + lineHeight: 24, + }, + modalInput: { + backgroundColor: '#0a0a14', + borderWidth: 1, + borderColor: '#333', + borderRadius: 8, + color: '#fff', + padding: 15, + fontSize: 18, + marginBottom: 25, + }, + modalButtons: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 15, + }, + modalBtn: { + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 8, + }, + modalBtnText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, }); diff --git a/app-release.apk b/apks/Schnappix_v1.0.1.apk similarity index 100% rename from app-release.apk rename to apks/Schnappix_v1.0.1.apk diff --git a/apks/Schnappix_v1.0.2.apk b/apks/Schnappix_v1.0.2.apk new file mode 100644 index 00000000..04ba0361 --- /dev/null +++ b/apks/Schnappix_v1.0.2.apk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06ee76bcdbc12d7990ebcb26acd4783d1130a56903249c18110843ab42096b8 +size 122755257 diff --git a/app.json b/app.json index 5c4c581a..f7f724ba 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "newArchEnabled": true, "name": "Schnappix", "slug": "Schnappix", - "version": "1.0.1", + "version": "1.0.2", "orientation": "landscape", "icon": "./Icon.png", "userInterfaceStyle": "dark", @@ -13,7 +13,7 @@ }, "android": { "package": "com.schnappix", - "versionCode": 2, + "versionCode": 3, "adaptiveIcon": { "backgroundColor": "#050510", "foregroundImage": "./Icon-padded.png" diff --git a/src/screens/AdminScreen.tsx b/src/screens/AdminScreen.tsx index b6d5742c..c6220698 100644 --- a/src/screens/AdminScreen.tsx +++ b/src/screens/AdminScreen.tsx @@ -337,81 +337,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS - - - - System & Hardware - - - Drucker IP-Adresse - - - - Admin-Passwort - - - - Tablet-Frontkamera als Fallback erzwingen - - - - App-Protokollierung (Logs) aktivieren - - - - Kiosk-Modus Status: - - {kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'} - - - - Device-Owner-Modus: - - {isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'} - - - {!isDeviceOwner && ( - - Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner. - - )} - - Bildschirm sperren (Kiosk-Modus) - - - - @@ -638,6 +563,91 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS )} + + + + System & Hardware + + + Drucker IP-Adresse + + + + Admin-Passwort + + + + Tablet-Frontkamera als Fallback erzwingen + + + + App-Protokollierung (Logs) aktivieren + + + + + + + + Sicherheit & Kiosk-Modus + + + Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen. + + + Kiosk-Modus Status: + + {kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'} + + + + Device-Owner-Modus: + + {isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'} + + + {!isDeviceOwner && ( + + Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner. + + )} + + Bildschirm sperren (Kiosk-Modus) + + + + (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); diff --git a/src/screens/PreviewScreen.tsx b/src/screens/PreviewScreen.tsx index 3399af7e..48262bfe 100644 --- a/src/screens/PreviewScreen.tsx +++ b/src/screens/PreviewScreen.tsx @@ -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; diff --git a/src/services/storage.ts b/src/services/storage.ts index 472456f2..6c6dfe6e 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -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 { 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 The permanent URI of the saved asset in the gallery. */ -export async function saveToGallery(localUri: string): Promise { +export async function saveToGallery(localUri: string, type: 'Raw' | 'Collage', eventName: string): Promise { 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 } } }