update: implement event-based photo storage, add event name validation, version apks
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import CameraScreen from './src/screens/CameraScreen';
|
import CameraScreen from './src/screens/CameraScreen';
|
||||||
import PreviewScreen from './src/screens/PreviewScreen';
|
import PreviewScreen from './src/screens/PreviewScreen';
|
||||||
@@ -27,6 +27,8 @@ export default function App() {
|
|||||||
const [photoUris, setPhotoUris] = useState<string[]>([]);
|
const [photoUris, setPhotoUris] = useState<string[]>([]);
|
||||||
const [captureHistory, setCaptureHistory] = useState<number[]>([]);
|
const [captureHistory, setCaptureHistory] = useState<number[]>([]);
|
||||||
const [settings, setSettings] = useState<AppSettings | null>(null);
|
const [settings, setSettings] = useState<AppSettings | null>(null);
|
||||||
|
const [showEventModal, setShowEventModal] = useState(false);
|
||||||
|
const [tempEventName, setTempEventName] = useState('');
|
||||||
|
|
||||||
// 1. Load configuration settings on app start
|
// 1. Load configuration settings on app start
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -58,6 +60,25 @@ export default function App() {
|
|||||||
|
|
||||||
// 2. Navigation Actions
|
// 2. Navigation Actions
|
||||||
const handleStartBooth = () => {
|
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([]);
|
setPhotoUris([]);
|
||||||
setCaptureHistory([]);
|
setCaptureHistory([]);
|
||||||
setCurrentScreen('camera');
|
setCurrentScreen('camera');
|
||||||
@@ -168,6 +189,7 @@ export default function App() {
|
|||||||
selectedFrameId={settings.selectedFrameId}
|
selectedFrameId={settings.selectedFrameId}
|
||||||
footerText={settings.footerText}
|
footerText={settings.footerText}
|
||||||
useFrontCamera={settings.useFrontCamera}
|
useFrontCamera={settings.useFrontCamera}
|
||||||
|
eventText={settings.eventText}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -200,6 +222,34 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Event Name Modal */}
|
||||||
|
<Modal visible={showEventModal} transparent animationType="fade">
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<Text style={styles.modalTitle}>Kein Event-Name gefunden</Text>
|
||||||
|
<Text style={styles.modalText}>
|
||||||
|
Möchtest du jetzt einen festlegen? (Ansonsten kann kein Foto gemacht werden).
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.modalInput}
|
||||||
|
placeholder="z.B. Jannik's Party"
|
||||||
|
placeholderTextColor="#666"
|
||||||
|
value={tempEventName}
|
||||||
|
onChangeText={setTempEventName}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<View style={styles.modalButtons}>
|
||||||
|
<TouchableOpacity style={[styles.modalBtn, { backgroundColor: '#333' }]} onPress={() => setShowEventModal(false)}>
|
||||||
|
<Text style={styles.modalBtnText}>Abbrechen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.modalBtn, { backgroundColor: '#ff2bd6' }]} onPress={handleSaveEventName}>
|
||||||
|
<Text style={styles.modalBtnText}>Speichern & Starten</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
@@ -216,4 +266,56 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: '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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -3,7 +3,7 @@
|
|||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"name": "Schnappix",
|
"name": "Schnappix",
|
||||||
"slug": "Schnappix",
|
"slug": "Schnappix",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"orientation": "landscape",
|
"orientation": "landscape",
|
||||||
"icon": "./Icon.png",
|
"icon": "./Icon.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"package": "com.schnappix",
|
"package": "com.schnappix",
|
||||||
"versionCode": 2,
|
"versionCode": 3,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#050510",
|
"backgroundColor": "#050510",
|
||||||
"foregroundImage": "./Icon-padded.png"
|
"foregroundImage": "./Icon-padded.png"
|
||||||
|
|||||||
+85
-75
@@ -337,81 +337,6 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<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.card}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
|
<FontAwesomeIcon icon={faBorderAll} size={16} color={THEME.colors.accent} />
|
||||||
@@ -638,6 +563,91 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
)}
|
)}
|
||||||
</View>
|
</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}>
|
<TouchableOpacity style={styles.saveBtn} onPress={handleSave} activeOpacity={0.8}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={THEME.gradient.primary}
|
colors={THEME.gradient.primary}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ interface CameraScreenProps {
|
|||||||
footerText: string;
|
footerText: string;
|
||||||
useFrontCamera: boolean;
|
useFrontCamera: boolean;
|
||||||
isObscured: boolean;
|
isObscured: boolean;
|
||||||
|
eventText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CameraScreen({
|
export default function CameraScreen({
|
||||||
@@ -76,6 +77,7 @@ export default function CameraScreen({
|
|||||||
footerText,
|
footerText,
|
||||||
useFrontCamera,
|
useFrontCamera,
|
||||||
isObscured,
|
isObscured,
|
||||||
|
eventText,
|
||||||
}: CameraScreenProps) {
|
}: CameraScreenProps) {
|
||||||
const [permission, requestPermission] = useCameraPermissions();
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
|
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
|
// Auto-save the high-res original raw photo to the gallery
|
||||||
try {
|
try {
|
||||||
await saveToGallery(capturedUri);
|
await saveToGallery(capturedUri, 'Raw', eventText);
|
||||||
console.log('Auto-saved high-res raw capture to gallery');
|
console.log('Auto-saved high-res raw capture to gallery');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to auto-save raw capture:', e);
|
console.error('Failed to auto-save raw capture:', e);
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export default function PreviewScreen({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setStatusMessage('Wird in Galerie gespeichert...');
|
setStatusMessage('Wird in Galerie gespeichert...');
|
||||||
await saveToGallery(printUri);
|
await saveToGallery(printUri, 'Collage', eventText);
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
|
|
||||||
setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉');
|
setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉');
|
||||||
@@ -331,7 +331,7 @@ export default function PreviewScreen({
|
|||||||
const capturedUri = await captureComposite(state.currentPhoto);
|
const capturedUri = await captureComposite(state.currentPhoto);
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
setStatusMessage('Wird gespeichert...');
|
setStatusMessage('Wird gespeichert...');
|
||||||
await saveToGallery(capturedUri);
|
await saveToGallery(capturedUri, 'Collage', eventText);
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
setStatusMessage('Erfolgreich gespeichert!');
|
setStatusMessage('Erfolgreich gespeichert!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -378,7 +378,7 @@ export default function PreviewScreen({
|
|||||||
const capturedUri = await captureComposite(currentPhoto);
|
const capturedUri = await captureComposite(currentPhoto);
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
setStatusMessage('Wird vor dem Wiederholen gespeichert...');
|
setStatusMessage('Wird vor dem Wiederholen gespeichert...');
|
||||||
await saveToGallery(capturedUri);
|
await saveToGallery(capturedUri, 'Collage', eventText);
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
|
|||||||
+53
-12
@@ -1,6 +1,5 @@
|
|||||||
import { getPermissionsAsync, requestPermissionsAsync, createAssetAsync, getAlbumAsync, createAlbumAsync, addAssetsToAlbumAsync } from 'expo-media-library';
|
import { getPermissionsAsync, requestPermissionsAsync, createAssetAsync, getAlbumAsync, createAlbumAsync, addAssetsToAlbumAsync } from 'expo-media-library';
|
||||||
|
import * as FileSystem from 'expo-file-system';
|
||||||
const ALBUM_NAME = 'Schnappix';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request necessary media library permissions.
|
* Request necessary media library permissions.
|
||||||
@@ -20,44 +19,86 @@ export async function requestStoragePermission(): Promise<boolean> {
|
|||||||
return false;
|
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 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.
|
* @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();
|
const hasPermission = await requestStoragePermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
throw new Error('Storage permission not granted. Cannot save photo to gallery.');
|
throw new Error('Storage permission not granted. Cannot save photo to gallery.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Create a media asset from the local file
|
// Clean event name for file system usage
|
||||||
const asset = await createAssetAsync(localUri);
|
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 {
|
try {
|
||||||
// 2. Check if the "Schnappix" album already exists
|
// 2. Check if the album already exists
|
||||||
const album = await getAlbumAsync(ALBUM_NAME);
|
const album = await getAlbumAsync(ALBUM_NAME);
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
// 3. If it doesn't exist, create it with our asset
|
// 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);
|
await createAlbumAsync(ALBUM_NAME, asset, true);
|
||||||
console.log(`Created new album "${ALBUM_NAME}" and copied photo.`);
|
console.log(`Created new album "${ALBUM_NAME}" and copied photo.`);
|
||||||
} else {
|
} else {
|
||||||
// 4. If it exists, add our asset to it
|
// 4. If it exists, add our asset to it
|
||||||
// copyAsset=true completely bypasses the Android 11+ OS prompt!
|
|
||||||
await addAssetsToAlbumAsync([asset], album, true);
|
await addAssetsToAlbumAsync([asset], album, true);
|
||||||
console.log(`Copied photo to existing album "${ALBUM_NAME}".`);
|
console.log(`Copied photo to existing album "${ALBUM_NAME}".`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return asset.uri;
|
return asset.uri;
|
||||||
} catch (e: any) {
|
} 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 {
|
try {
|
||||||
|
const fallbackAlbum = await getAlbumAsync(fallbackAlbumName);
|
||||||
|
if (!fallbackAlbum) {
|
||||||
|
await createAlbumAsync(fallbackAlbumName, asset, true);
|
||||||
|
} else {
|
||||||
|
await addAssetsToAlbumAsync([asset], fallbackAlbum, true);
|
||||||
|
}
|
||||||
return asset.uri;
|
return asset.uri;
|
||||||
} catch {
|
} catch (fallbackErr) {
|
||||||
return asset.id;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user