Compare commits
2
Commits
99634bb480
...
305d8fd449
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
305d8fd449 | ||
|
|
d8e397eac1 |
@@ -39,3 +39,8 @@ yarn-error.*
|
|||||||
# generated native folders
|
# generated native folders
|
||||||
/ios
|
/ios
|
||||||
/android
|
/android
|
||||||
|
|
||||||
|
# Gradle/Android build folders
|
||||||
|
**/build/
|
||||||
|
**/.gradle/
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { StyleSheet, View, SafeAreaView, StatusBar } from 'react-native';
|
import { StyleSheet, View, SafeAreaView, StatusBar } from 'react-native';
|
||||||
import HomeScreen from './src/screens/HomeScreen';
|
|
||||||
import CameraScreen from './src/screens/CameraScreen';
|
import CameraScreen from './src/screens/CameraScreen';
|
||||||
import PreviewScreen from './src/screens/PreviewScreen';
|
import PreviewScreen from './src/screens/PreviewScreen';
|
||||||
import AdminScreen from './src/screens/AdminScreen';
|
import AdminScreen from './src/screens/AdminScreen';
|
||||||
@@ -87,6 +86,7 @@ export default function App() {
|
|||||||
countdownDuration={settings.countdownDuration}
|
countdownDuration={settings.countdownDuration}
|
||||||
onPhotoCaptured={handlePhotoCaptured}
|
onPhotoCaptured={handlePhotoCaptured}
|
||||||
onCancel={handleReset}
|
onCancel={handleReset}
|
||||||
|
isIdle={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'preview':
|
case 'preview':
|
||||||
@@ -109,8 +109,12 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<HomeScreen
|
<CameraScreen
|
||||||
onStart={handleStartBooth}
|
countdownDuration={settings.countdownDuration}
|
||||||
|
onPhotoCaptured={handlePhotoCaptured}
|
||||||
|
onCancel={handleReset}
|
||||||
|
isIdle={true}
|
||||||
|
onStartBooth={handleStartBooth}
|
||||||
onNavigateToAdmin={handleOpenAdmin}
|
onNavigateToAdmin={handleOpenAdmin}
|
||||||
adminPassword={settings.adminPassword}
|
adminPassword={settings.adminPassword}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"slug": "Schnappix",
|
"slug": "Schnappix",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "landscape",
|
"orientation": "landscape",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./Icon.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"package": "com.schnappix",
|
"package": "com.schnappix",
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#121214",
|
"backgroundColor": "#121214",
|
||||||
"foregroundImage": "./assets/icon.png"
|
"foregroundImage": "./Icon.png"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.CAMERA",
|
"android.permission.CAMERA",
|
||||||
@@ -48,15 +48,14 @@
|
|||||||
{
|
{
|
||||||
"android": {
|
"android": {
|
||||||
"extraMavenRepos": [
|
"extraMavenRepos": [
|
||||||
"https://jitpack.io",
|
"https://jitpack.io"
|
||||||
"https://jcenter.bintray.com"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"./plugins/withKioskAdmin",
|
"./plugins/withKioskAdmin",
|
||||||
"./plugins/withUsbCamera",
|
"./plugins/withUsbCamera",
|
||||||
"./plugins/withSettingsRepositories"
|
"./plugins/withLocalAars"
|
||||||
],
|
],
|
||||||
"extra": {
|
"extra": {
|
||||||
"eas": {
|
"eas": {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ dependencies {
|
|||||||
exclude group: 'com.gyf.immersionbar', module: 'immersionbar'
|
exclude group: 'com.gyf.immersionbar', module: 'immersionbar'
|
||||||
exclude group: 'com.zlc.glide', module: 'webpdecoder'
|
exclude group: 'com.zlc.glide', module: 'webpdecoder'
|
||||||
}
|
}
|
||||||
// Local AAR dependencies downloaded to avoid JCenter resolution issues
|
// Local AAR dependencies compiled only; injected into app build.gradle by plugin
|
||||||
implementation files('libs/immersionbar-3.0.0.aar')
|
compileOnly files('libs/immersionbar-3.0.0.aar')
|
||||||
implementation files('libs/webpdecoder-1.6.4.9.0.aar')
|
compileOnly files('libs/webpdecoder-1.6.4.9.0.aar')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
const { withAppBuildGradle } = require('@expo/config-plugins');
|
||||||
|
|
||||||
|
const withLocalAars = (config) => {
|
||||||
|
return withAppBuildGradle(config, (config) => {
|
||||||
|
let contents = config.modResults.contents;
|
||||||
|
|
||||||
|
// Inject the local AAR files into dependencies block
|
||||||
|
const dependencyBlock = `
|
||||||
|
// Added by withLocalAars plugin
|
||||||
|
implementation files("../../modules/usb-camera/android/libs/immersionbar-3.0.0.aar")
|
||||||
|
implementation files("../../modules/usb-camera/android/libs/webpdecoder-1.6.4.9.0.aar")
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!contents.includes('immersionbar-3.0.0.aar')) {
|
||||||
|
contents = contents.replace(
|
||||||
|
/dependencies\s*\{/,
|
||||||
|
`dependencies {${dependencyBlock}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.modResults.contents = contents;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = withLocalAars;
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const { withProjectBuildGradle, withSettingsGradle } = require('@expo/config-plugins');
|
|
||||||
|
|
||||||
const withSettingsRepositories = (config) => {
|
|
||||||
// 1. Modify settings.gradle (settings-level repositories)
|
|
||||||
config = withSettingsGradle(config, (config) => {
|
|
||||||
let contents = config.modResults.contents;
|
|
||||||
const reposBlock = `
|
|
||||||
// Added by withSettingsRepositories plugin
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://jitpack.io' }
|
|
||||||
maven { url 'https://jcenter.bintray.com' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
if (!contents.includes('https://jcenter.bintray.com')) {
|
|
||||||
contents += reposBlock;
|
|
||||||
}
|
|
||||||
config.modResults.contents = contents;
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Modify root build.gradle (project-level repositories)
|
|
||||||
config = withProjectBuildGradle(config, (config) => {
|
|
||||||
let contents = config.modResults.contents;
|
|
||||||
|
|
||||||
// Inject jcenter and jitpack into the allprojects.repositories block
|
|
||||||
if (!contents.includes('https://jcenter.bintray.com')) {
|
|
||||||
contents = contents.replace(
|
|
||||||
/maven\s*\{\s*url\s*['"]https:\/\/www\.jitpack\.io['"]\s*\}/g,
|
|
||||||
`maven { url 'https://www.jitpack.io' }
|
|
||||||
maven { url 'https://jcenter.bintray.com' }
|
|
||||||
maven { url 'https://jitpack.io' }`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.modResults.contents = contents;
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = withSettingsRepositories;
|
|
||||||
+43
-36
@@ -9,6 +9,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Alert,
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { THEME } from '../styles/theme';
|
import { THEME } from '../styles/theme';
|
||||||
import KioskMode from '../../modules/kiosk-mode';
|
import KioskMode from '../../modules/kiosk-mode';
|
||||||
import { AppSettings, saveSettings } from '../services/settings';
|
import { AppSettings, saveSettings } from '../services/settings';
|
||||||
@@ -53,48 +54,54 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
if (success) {
|
if (success) {
|
||||||
setKioskActive(true);
|
setKioskActive(true);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Kiosk Mode Started',
|
'Kiosk-Modus gestartet',
|
||||||
isDeviceOwner
|
isDeviceOwner
|
||||||
? 'True Kiosk Mode active. System navigation buttons are completely locked.'
|
? 'Echter Kiosk-Modus aktiv. Die System-Navigationsleiste ist vollständig gesperrt.'
|
||||||
: 'Screen Pinning initiated. Accept the OS prompt to lock the screen.'
|
: 'Bildschirmheftung (Screen Pinning) gestartet. Bitte bestätige das Systemfenster, um den Bildschirm zu sperren.'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('Error', 'Failed to start Kiosk Mode.');
|
Alert.alert('Fehler', 'Kiosk-Modus konnte nicht gestartet werden.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const success = KioskMode.stopKiosk();
|
const success = KioskMode.stopKiosk();
|
||||||
if (success) {
|
if (success) {
|
||||||
setKioskActive(false);
|
setKioskActive(false);
|
||||||
Alert.alert('Kiosk Mode Stopped', 'System navigation buttons are unlocked.');
|
Alert.alert('Kiosk-Modus beendet', 'Die Navigationstasten des Systems sind wieder freigegeben.');
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('Error', 'Failed to stop Kiosk Mode.');
|
Alert.alert('Fehler', 'Kiosk-Modus konnte nicht beendet werden.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Alert.alert('Native Error', e.message || 'Kiosk module error');
|
Alert.alert('Nativer Fehler', e.message || 'Fehler im Kiosk-Modul');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const duration = parseInt(countdown, 10);
|
const duration = parseInt(countdown, 10);
|
||||||
if (isNaN(duration) || duration < 1 || duration > 30) {
|
if (isNaN(duration) || duration < 1 || duration > 30) {
|
||||||
Alert.alert('Invalid Input', 'Countdown duration must be a number between 1 and 30 seconds.');
|
Alert.alert('Ungültige Eingabe', 'Die Countdown-Dauer muss eine Zahl zwischen 1 und 30 Sekunden sein.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!printerIp.trim()) {
|
const ipTrimmed = printerIp.trim();
|
||||||
Alert.alert('Invalid Input', 'Printer IP address cannot be empty.');
|
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||||
|
if (!ipTrimmed) {
|
||||||
|
Alert.alert('Ungültige Eingabe', 'Die Drucker-IP-Adresse darf nicht leer sein.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ipRegex.test(ipTrimmed)) {
|
||||||
|
Alert.alert('Ungültige Eingabe', 'Bitte gib eine gültige IP-Adresse ein (z.B. 192.168.1.100).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password.trim() || password.length < 4) {
|
if (!password.trim() || password.length < 4) {
|
||||||
Alert.alert('Invalid Input', 'Admin password must be at least 4 digits.');
|
Alert.alert('Ungültige Eingabe', 'Das Admin-Passwort muss mindestens 4 Ziffern lang sein.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated: AppSettings = {
|
const updated: AppSettings = {
|
||||||
countdownDuration: duration,
|
countdownDuration: duration,
|
||||||
printerIp: printerIp.trim(),
|
printerIp: ipTrimmed,
|
||||||
adminPassword: password.trim(),
|
adminPassword: password.trim(),
|
||||||
kioskModeEnabled: kioskActive,
|
kioskModeEnabled: kioskActive,
|
||||||
};
|
};
|
||||||
@@ -102,33 +109,33 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
try {
|
try {
|
||||||
await saveSettings(updated);
|
await saveSettings(updated);
|
||||||
onSave(updated);
|
onSave(updated);
|
||||||
Alert.alert('Success', 'Settings saved successfully!');
|
Alert.alert('Erfolg', 'Einstellungen erfolgreich gespeichert!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Alert.alert('Error', 'Failed to save settings to disk.');
|
Alert.alert('Fehler', 'Einstellungen konnten nicht gespeichert werden.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||||
<View style={styles.headerRow}>
|
<View style={styles.headerRow}>
|
||||||
<Text style={styles.title}>SCHNAPPIX SETTINGS</Text>
|
<Text style={styles.title}>SCHNAPPIX EINSTELLUNGEN</Text>
|
||||||
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
|
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
|
||||||
<Text style={styles.closeBtnText}>Back to Booth</Text>
|
<Text style={styles.closeBtnText}>Zurück zur Fotobox</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
{/* Printer Configurations */}
|
{/* Printer Configurations */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Printer Configuration</Text>
|
<Text style={styles.cardTitle}>Drucker-Konfiguration</Text>
|
||||||
<Text style={styles.cardDesc}>
|
<Text style={styles.cardDesc}>
|
||||||
Configure the local IP address of your Wi-Fi connected Canon CP1300 printer.
|
Konfiguriere die lokale IP-Adresse deines über WLAN verbundenen Canon CP1300 Druckers.
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Printer IP Address</Text>
|
<Text style={styles.label}>Drucker IP-Adresse</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="e.g. 192.168.1.100"
|
placeholder="z.B. 192.168.1.100"
|
||||||
placeholderTextColor={THEME.colors.textMuted}
|
placeholderTextColor={THEME.colors.textMuted}
|
||||||
value={printerIp}
|
value={printerIp}
|
||||||
onChangeText={setPrinterIp}
|
onChangeText={setPrinterIp}
|
||||||
@@ -139,12 +146,12 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
|
|
||||||
{/* Booth Behavior */}
|
{/* Booth Behavior */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Photo Booth Behavior</Text>
|
<Text style={styles.cardTitle}>Fotobox-Verhalten</Text>
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Countdown Duration (seconds)</Text>
|
<Text style={styles.label}>Countdown-Dauer (Sekunden)</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Default: 3"
|
placeholder="Standard: 3"
|
||||||
placeholderTextColor={THEME.colors.textMuted}
|
placeholderTextColor={THEME.colors.textMuted}
|
||||||
value={countdown}
|
value={countdown}
|
||||||
onChangeText={setCountdown}
|
onChangeText={setCountdown}
|
||||||
@@ -152,10 +159,10 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.inputGroup}>
|
<View style={styles.inputGroup}>
|
||||||
<Text style={styles.label}>Admin Panel Password</Text>
|
<Text style={styles.label}>Admin-Passwort</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Default: 1234"
|
placeholder="Standard: 1234"
|
||||||
placeholderTextColor={THEME.colors.textMuted}
|
placeholderTextColor={THEME.colors.textMuted}
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
value={password}
|
value={password}
|
||||||
@@ -167,33 +174,33 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
|
|
||||||
{/* Security & Kiosk Mode */}
|
{/* Security & Kiosk Mode */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Kiosk Mode & Security</Text>
|
<Text style={styles.cardTitle}>Kiosk-Modus & Sicherheit</Text>
|
||||||
<Text style={styles.cardDesc}>
|
<Text style={styles.cardDesc}>
|
||||||
Lock the screen to prevent guests from closing the app or opening system settings.
|
Sperre den Bildschirm, um zu verhindern, dass Gäste die App schließen oder auf die Systemeinstellungen zugreifen.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.statusRow}>
|
<View style={styles.statusRow}>
|
||||||
<Text style={styles.statusLabel}>Kiosk Mode State:</Text>
|
<Text style={styles.statusLabel}>Kiosk-Modus Status:</Text>
|
||||||
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}>
|
<Text style={[styles.statusValue, kioskActive ? styles.statusActive : styles.statusInactive]}>
|
||||||
{kioskActive ? 'LOCKED 🔒' : 'UNLOCKED 🔓'}
|
{kioskActive ? 'GESPERRT 🔒' : 'ENTSPERRT 🔓'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.statusRow}>
|
<View style={styles.statusRow}>
|
||||||
<Text style={styles.statusLabel}>Device Owner Mode:</Text>
|
<Text style={styles.statusLabel}>Device-Owner-Modus:</Text>
|
||||||
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}>
|
<Text style={[styles.statusValue, isDeviceOwner ? styles.statusActive : styles.statusWarning]}>
|
||||||
{isDeviceOwner ? 'ACTIVE (True Lock) ✅' : 'INACTIVE (Screen Pinning Fallback) ⚠️'}
|
{isDeviceOwner ? 'AKTIV (Echte Sperre) ✅' : 'INAKTIV (Screen Pinning Fallback) ⚠️'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{!isDeviceOwner && (
|
{!isDeviceOwner && (
|
||||||
<Text style={styles.kioskWarningText}>
|
<Text style={styles.kioskWarningText}>
|
||||||
Note: To enable True Lock task mode without prompt bypasses, make the app device owner using ADB.
|
Hinweis: Um den echten Lock-Task-Modus ohne Bestätigungsaufforderung zu aktivieren, mache die App mittels ADB zum Device Owner.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.toggleRow}>
|
<View style={styles.toggleRow}>
|
||||||
<Text style={styles.toggleLabel}>Lock Screen (Kiosk Mode)</Text>
|
<Text style={styles.toggleLabel}>Bildschirm sperren (Kiosk-Modus)</Text>
|
||||||
<Switch
|
<Switch
|
||||||
value={kioskActive}
|
value={kioskActive}
|
||||||
onValueChange={handleKioskToggle}
|
onValueChange={handleKioskToggle}
|
||||||
@@ -204,10 +211,10 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.saveBtn} onPress={handleSave}>
|
<TouchableOpacity style={styles.saveBtn} onPress={handleSave}>
|
||||||
<Text style={styles.saveBtnText}>SAVE SETTINGS</Text>
|
<Text style={styles.saveBtnText}>EINSTELLUNGEN SPEICHERN</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+389
-57
@@ -1,26 +1,70 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { StyleSheet, Text, View, TouchableOpacity, Platform } from 'react-native';
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
TouchableOpacity,
|
||||||
|
Platform,
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
} from 'react-native';
|
||||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||||
import * as FileSystem from 'expo-file-system';
|
import { File, Paths } from 'expo-file-system';
|
||||||
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { THEME } from '../styles/theme';
|
import { THEME } from '../styles/theme';
|
||||||
import { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera';
|
import { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera';
|
||||||
|
import { saveToGallery } from '../services/storage';
|
||||||
|
|
||||||
interface CameraScreenProps {
|
interface CameraScreenProps {
|
||||||
countdownDuration: number;
|
countdownDuration: number;
|
||||||
onPhotoCaptured: (uri: string) => void;
|
onPhotoCaptured: (uri: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
isIdle?: boolean;
|
||||||
|
onStartBooth?: () => void;
|
||||||
|
onNavigateToAdmin?: () => void;
|
||||||
|
adminPassword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCancel }: CameraScreenProps) {
|
export default function CameraScreen({
|
||||||
|
countdownDuration,
|
||||||
|
onPhotoCaptured,
|
||||||
|
onCancel,
|
||||||
|
isIdle = false,
|
||||||
|
onStartBooth,
|
||||||
|
onNavigateToAdmin,
|
||||||
|
adminPassword = '1234',
|
||||||
|
}: CameraScreenProps) {
|
||||||
const [permission, requestPermission] = useCameraPermissions();
|
const [permission, requestPermission] = useCameraPermissions();
|
||||||
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
|
const [isUsbConnected, setIsUsbConnected] = useState<boolean>(false);
|
||||||
const [countdown, setCountdown] = useState<number | string>('');
|
const [countdown, setCountdown] = useState<number | string>('');
|
||||||
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
const [isCapturing, setIsCapturing] = useState<boolean>(false);
|
||||||
const [hasStarted, setHasStarted] = useState<boolean>(false);
|
const [hasStarted, setHasStarted] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Admin Modal States
|
||||||
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
||||||
|
const [enteredPassword, setEnteredPassword] = useState('');
|
||||||
|
const [errorText, setErrorText] = useState('');
|
||||||
|
|
||||||
const usbCameraRef = useRef<UsbCameraRef>(null);
|
const usbCameraRef = useRef<UsbCameraRef>(null);
|
||||||
const expoCameraRef = useRef<any>(null);
|
const expoCameraRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// Reanimated countdown pulse animation
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countdown !== '') {
|
||||||
|
scale.value = 0.5;
|
||||||
|
scale.value = withSpring(1.25, { damping: 10, stiffness: 120 }, () => {
|
||||||
|
scale.value = withTiming(1.0, { duration: 150 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [countdown]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{ scale: scale.value }],
|
||||||
|
}));
|
||||||
|
|
||||||
// 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)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!permission || !permission.granted) {
|
if (!permission || !permission.granted) {
|
||||||
@@ -28,10 +72,9 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
}
|
}
|
||||||
}, [permission]);
|
}, [permission]);
|
||||||
|
|
||||||
// 2. Check if a USB camera is connected.
|
// 2. Check if a USB camera is connected.
|
||||||
// We can query the USB module or attempt to check every second in the background.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let checkInterval: NodeJS.Timeout;
|
let checkInterval: any;
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
const checkConnection = async () => {
|
const checkConnection = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -40,12 +83,10 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
setIsUsbConnected(connected);
|
setIsUsbConnected(connected);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If native view manager is not loaded or errors, fallback to false
|
|
||||||
setIsUsbConnected(false);
|
setIsUsbConnected(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check initially and run an interval
|
|
||||||
checkConnection();
|
checkConnection();
|
||||||
checkInterval = setInterval(checkConnection, 2000);
|
checkInterval = setInterval(checkConnection, 2000);
|
||||||
}
|
}
|
||||||
@@ -55,13 +96,19 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 3. Start the countdown on screen load
|
// 3. Start the countdown on screen load (only when NOT idle)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
if (isIdle) {
|
||||||
|
setHasStarted(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer: any;
|
||||||
let count = countdownDuration;
|
let count = countdownDuration;
|
||||||
|
|
||||||
setCountdown(count);
|
setCountdown(count);
|
||||||
setHasStarted(true);
|
setHasStarted(true);
|
||||||
|
setIsCapturing(false);
|
||||||
|
|
||||||
const runTimer = () => {
|
const runTimer = () => {
|
||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
@@ -69,7 +116,7 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
setCountdown(count);
|
setCountdown(count);
|
||||||
timer = setTimeout(runTimer, 1000);
|
timer = setTimeout(runTimer, 1000);
|
||||||
} else if (count === 1) {
|
} else if (count === 1) {
|
||||||
setCountdown('Cheese! 📸');
|
setCountdown('Bitte lächeln! 📸');
|
||||||
setIsCapturing(true);
|
setIsCapturing(true);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
capture();
|
capture();
|
||||||
@@ -82,18 +129,28 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
return () => {
|
return () => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [countdownDuration]);
|
}, [countdownDuration, isIdle]);
|
||||||
|
|
||||||
// 4. Capture photo function
|
// 4. Capture photo function
|
||||||
const capture = async () => {
|
const capture = async () => {
|
||||||
const filename = `photo_${Date.now()}.jpg`;
|
const filename = `photo_${Date.now()}.jpg`;
|
||||||
const tempUri = `${FileSystem.cacheDirectory}${filename}`;
|
const tempFile = new File(Paths.cache, filename);
|
||||||
|
const tempUri = tempFile.uri;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Platform.OS === 'android' && isUsbConnected && usbCameraRef.current) {
|
if (Platform.OS === 'android' && isUsbConnected && usbCameraRef.current) {
|
||||||
// USB Camera Capture
|
// USB Camera Capture
|
||||||
console.log('Capturing from USB Camera...');
|
console.log('Capturing from USB Camera...');
|
||||||
const path = await usbCameraRef.current.takePicture(tempUri);
|
const path = await usbCameraRef.current.takePicture(tempUri);
|
||||||
|
|
||||||
|
// Auto-save individual photo directly to gallery
|
||||||
|
try {
|
||||||
|
await saveToGallery(path);
|
||||||
|
console.log('Auto-saved USB capture to gallery');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to auto-save USB capture:', e);
|
||||||
|
}
|
||||||
|
|
||||||
onPhotoCaptured(path);
|
onPhotoCaptured(path);
|
||||||
} else {
|
} else {
|
||||||
// Fallback Camera Capture
|
// Fallback Camera Capture
|
||||||
@@ -103,22 +160,47 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
quality: 0.95,
|
quality: 0.95,
|
||||||
skipProcessing: false,
|
skipProcessing: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-save individual photo directly to gallery
|
||||||
|
try {
|
||||||
|
await saveToGallery(photo.uri);
|
||||||
|
console.log('Auto-saved fallback capture to gallery');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to auto-save fallback capture:', e);
|
||||||
|
}
|
||||||
|
|
||||||
onPhotoCaptured(photo.uri);
|
onPhotoCaptured(photo.uri);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Camera ref is not available.');
|
throw new Error('Kamera-Referenz ist nicht verfügbar.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Capture failed:', error);
|
console.error('Capture failed:', error);
|
||||||
alert('Failed to capture photo: ' + error.message);
|
alert('Fehler beim Aufnehmen des Fotos: ' + error.message);
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSettingsTap = () => {
|
||||||
|
setEnteredPassword('');
|
||||||
|
setErrorText('');
|
||||||
|
setPasswordModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordSubmit = () => {
|
||||||
|
if (enteredPassword === adminPassword) {
|
||||||
|
setPasswordModalVisible(false);
|
||||||
|
onNavigateToAdmin?.();
|
||||||
|
} else {
|
||||||
|
setErrorText('Falsches Passwort');
|
||||||
|
setEnteredPassword('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!permission) {
|
if (!permission) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.infoText}>Loading permissions...</Text>
|
<Text style={styles.infoText}>Berechtigungen werden geladen...</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -126,20 +208,124 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
if (!permission.granted && !isUsbConnected) {
|
if (!permission.granted && !isUsbConnected) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.infoText}>We need your permission to show the camera</Text>
|
<Text style={styles.infoText}>Wir benötigen deine Erlaubnis, um die Kamera anzuzeigen</Text>
|
||||||
<TouchableOpacity style={styles.btn} onPress={requestPermission}>
|
<TouchableOpacity style={styles.btn} onPress={requestPermission}>
|
||||||
<Text style={styles.btnText}>Grant Permission</Text>
|
<Text style={styles.btnText}>Erlaubnis erteilen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={[styles.btn, styles.cancelBtn]} onPress={onCancel}>
|
<TouchableOpacity style={[styles.btn, styles.cancelBtn]} onPress={onCancel}>
|
||||||
<Text style={styles.btnText}>Go Back</Text>
|
<Text style={styles.btnText}>Zurück</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isIdle) {
|
||||||
|
// Home Overlay (frosty glass box on top of the camera stream)
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback onPress={onStartBooth}>
|
||||||
|
<View style={styles.idleOverlayContainer}>
|
||||||
|
{/* Settings button in the top-right corner */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.settingsButton}
|
||||||
|
onPress={handleSettingsTap}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.settingsIcon}>⚙️</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.welcomeBox}>
|
||||||
|
<Text style={styles.logo}>SCHNAPPIX</Text>
|
||||||
|
<Text style={styles.welcomeTitle}>Willkommen zu unserer Feier!</Text>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<View style={styles.startButton}>
|
||||||
|
<Text style={styles.startButtonText}>ZUM STARTEN ÜBERALL TIPPEN</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.welcomeFooter}>Fotos machen • Collage erstellen • Sofort drucken</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Password Prompt Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={passwordModalVisible}
|
||||||
|
transparent={true}
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setPasswordModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<Text style={styles.modalTitle}>Admin-Zugang</Text>
|
||||||
|
<Text style={styles.modalSub}>Passwort eingeben, um Einstellungen zu öffnen</Text>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
secureTextEntry
|
||||||
|
placeholder="Passwort"
|
||||||
|
placeholderTextColor={THEME.colors.textMuted}
|
||||||
|
value={enteredPassword}
|
||||||
|
onChangeText={setEnteredPassword}
|
||||||
|
keyboardType="numeric"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errorText ? <Text style={styles.error}>{errorText}</Text> : null}
|
||||||
|
|
||||||
|
<View style={styles.modalButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.cancelButton]}
|
||||||
|
onPress={() => setPasswordModalVisible(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Abbrechen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.confirmButton]}
|
||||||
|
onPress={handlePasswordSubmit}
|
||||||
|
>
|
||||||
|
<Text style={styles.buttonText}>Öffnen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture Mode overlays (Countdown and Cancel button)
|
||||||
|
return (
|
||||||
|
<View style={styles.captureOverlayContainer} pointerEvents="box-none">
|
||||||
|
{/* Source indicator */}
|
||||||
|
<View style={styles.hudContainer}>
|
||||||
|
<Text style={styles.cameraSourceIndicator}>
|
||||||
|
Kamera: {isUsbConnected ? 'Externe USB-Kamera' : 'Tablet-Frontkamera (Fallback)'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Countdown overlay */}
|
||||||
|
{hasStarted && (
|
||||||
|
<View style={styles.countdownOverlay} pointerEvents="none">
|
||||||
|
<Animated.View style={[styles.countdownBox, animatedStyle]}>
|
||||||
|
<Text style={styles.countdownText}>{countdown}</Text>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
{!isCapturing && (
|
||||||
|
<TouchableOpacity style={styles.backButton} onPress={onCancel}>
|
||||||
|
<Text style={styles.backButtonText}>ABBRECHEN</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||||
{/* Camera Preview Area */}
|
{/* Camera Preview Background */}
|
||||||
<View style={styles.previewContainer}>
|
<View style={styles.previewContainer}>
|
||||||
{Platform.OS === 'android' && isUsbConnected ? (
|
{Platform.OS === 'android' && isUsbConnected ? (
|
||||||
<UsbCameraView ref={usbCameraRef} style={styles.cameraPreview} />
|
<UsbCameraView ref={usbCameraRef} style={styles.cameraPreview} />
|
||||||
@@ -152,29 +338,9 @@ export default function CameraScreen({ countdownDuration, onPhotoCaptured, onCan
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Camera Info HUD */}
|
{/* Render UI controls/overlays on top */}
|
||||||
<View style={styles.hudContainer}>
|
{renderContent()}
|
||||||
<Text style={styles.cameraSourceIndicator}>
|
</Animated.View>
|
||||||
Source: {isUsbConnected ? 'External USB Camera' : 'Front Tablet Camera (Fallback)'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Countdown overlay */}
|
|
||||||
{hasStarted && (
|
|
||||||
<View style={styles.overlay}>
|
|
||||||
<View style={styles.countdownBox}>
|
|
||||||
<Text style={styles.countdownText}>{countdown}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Cancel Button */}
|
|
||||||
{!isCapturing && (
|
|
||||||
<TouchableOpacity style={styles.backButton} onPress={onCancel}>
|
|
||||||
<Text style={styles.backButtonText}>CANCEL</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,32 +348,119 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
previewContainer: {
|
previewContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
},
|
},
|
||||||
cameraPreview: {
|
cameraPreview: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
|
idleOverlayContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
},
|
||||||
|
captureOverlayContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
settingsButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 24,
|
||||||
|
right: 24,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: THEME.borderRadius.round,
|
||||||
|
backgroundColor: 'rgba(30, 30, 35, 0.75)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 999,
|
||||||
|
},
|
||||||
|
settingsIcon: {
|
||||||
|
fontSize: 24,
|
||||||
|
color: THEME.colors.text,
|
||||||
|
},
|
||||||
|
welcomeBox: {
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: THEME.spacing.xl,
|
||||||
|
borderRadius: THEME.borderRadius.lg,
|
||||||
|
backgroundColor: 'rgba(18, 18, 20, 0.8)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||||
|
width: '60%',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 10 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 20,
|
||||||
|
elevation: 10,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
fontSize: 54,
|
||||||
|
fontWeight: '900',
|
||||||
|
color: THEME.colors.primary,
|
||||||
|
letterSpacing: 8,
|
||||||
|
marginBottom: THEME.spacing.sm,
|
||||||
|
textShadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
textShadowOffset: { width: 0, height: 2 },
|
||||||
|
textShadowRadius: 4,
|
||||||
|
},
|
||||||
|
welcomeTitle: {
|
||||||
|
fontSize: 22,
|
||||||
|
color: THEME.colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: THEME.spacing.lg,
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
width: 60,
|
||||||
|
height: 3,
|
||||||
|
backgroundColor: THEME.colors.accent,
|
||||||
|
marginBottom: THEME.spacing.xl,
|
||||||
|
borderRadius: THEME.borderRadius.sm,
|
||||||
|
},
|
||||||
|
startButton: {
|
||||||
|
paddingVertical: THEME.spacing.md,
|
||||||
|
paddingHorizontal: THEME.spacing.xl,
|
||||||
|
backgroundColor: THEME.colors.primary,
|
||||||
|
borderRadius: THEME.borderRadius.round,
|
||||||
|
marginBottom: THEME.spacing.xl,
|
||||||
|
},
|
||||||
|
startButtonText: {
|
||||||
|
color: THEME.colors.text,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: 2,
|
||||||
|
},
|
||||||
|
welcomeFooter: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: THEME.colors.textMuted,
|
||||||
|
letterSpacing: 1,
|
||||||
|
},
|
||||||
hudContainer: {
|
hudContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||||
paddingVertical: THEME.spacing.xs,
|
paddingVertical: THEME.spacing.xs,
|
||||||
paddingHorizontal: THEME.spacing.md,
|
paddingHorizontal: THEME.spacing.md,
|
||||||
borderRadius: THEME.borderRadius.sm,
|
borderRadius: THEME.borderRadius.sm,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
},
|
},
|
||||||
cameraSourceIndicator: {
|
cameraSourceIndicator: {
|
||||||
color: THEME.colors.text,
|
color: THEME.colors.text,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
overlay: {
|
countdownOverlay: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -215,37 +468,39 @@ const styles = StyleSheet.create({
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
||||||
},
|
},
|
||||||
countdownBox: {
|
countdownBox: {
|
||||||
backgroundColor: THEME.colors.glassBackground,
|
backgroundColor: 'rgba(20, 20, 25, 0.85)',
|
||||||
paddingVertical: THEME.spacing.xl,
|
paddingVertical: THEME.spacing.xl,
|
||||||
paddingHorizontal: THEME.spacing.xxl,
|
paddingHorizontal: THEME.spacing.xxl,
|
||||||
borderRadius: THEME.borderRadius.lg,
|
borderRadius: THEME.borderRadius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: THEME.colors.border,
|
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 10 },
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 15,
|
||||||
|
elevation: 8,
|
||||||
},
|
},
|
||||||
countdownText: {
|
countdownText: {
|
||||||
fontSize: 72,
|
fontSize: 64,
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
color: THEME.colors.text,
|
color: THEME.colors.text,
|
||||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
textAlign: 'center',
|
||||||
textShadowOffset: { width: -1, height: 1 },
|
|
||||||
textShadowRadius: 10,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 24,
|
top: 24,
|
||||||
left: 24,
|
left: 24,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
backgroundColor: 'rgba(30, 30, 35, 0.75)',
|
||||||
paddingVertical: THEME.spacing.sm,
|
paddingVertical: THEME.spacing.sm,
|
||||||
paddingHorizontal: THEME.spacing.lg,
|
paddingHorizontal: THEME.spacing.lg,
|
||||||
borderRadius: THEME.borderRadius.round,
|
borderRadius: THEME.borderRadius.round,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: THEME.colors.border,
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
},
|
},
|
||||||
backButtonText: {
|
backButtonText: {
|
||||||
color: THEME.colors.text,
|
color: THEME.colors.text,
|
||||||
@@ -277,4 +532,81 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: THEME.colors.overlay,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
width: 340,
|
||||||
|
padding: THEME.spacing.lg,
|
||||||
|
borderRadius: THEME.borderRadius.md,
|
||||||
|
backgroundColor: THEME.colors.surfaceSecondary,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: THEME.colors.border,
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 10 },
|
||||||
|
shadowOpacity: 0.4,
|
||||||
|
shadowRadius: 15,
|
||||||
|
elevation: 10,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: THEME.colors.text,
|
||||||
|
marginBottom: THEME.spacing.xs,
|
||||||
|
},
|
||||||
|
modalSub: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: THEME.colors.textMuted,
|
||||||
|
marginBottom: THEME.spacing.md,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
width: '100%',
|
||||||
|
height: 50,
|
||||||
|
backgroundColor: THEME.colors.surface,
|
||||||
|
borderColor: THEME.colors.border,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: THEME.borderRadius.sm,
|
||||||
|
color: THEME.colors.text,
|
||||||
|
paddingHorizontal: THEME.spacing.md,
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: THEME.spacing.sm,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: THEME.colors.error,
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: THEME.spacing.sm,
|
||||||
|
},
|
||||||
|
modalButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
marginTop: THEME.spacing.sm,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flex: 1,
|
||||||
|
height: 45,
|
||||||
|
borderRadius: THEME.borderRadius.sm,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginHorizontal: THEME.spacing.xs,
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: THEME.colors.border,
|
||||||
|
},
|
||||||
|
confirmButton: {
|
||||||
|
backgroundColor: THEME.colors.primary,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: THEME.colors.text,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
TouchableOpacity,
|
|
||||||
Modal,
|
|
||||||
TextInput,
|
|
||||||
TouchableWithoutFeedback,
|
|
||||||
} from 'react-native';
|
|
||||||
import { THEME } from '../styles/theme';
|
|
||||||
|
|
||||||
interface HomeScreenProps {
|
|
||||||
onStart: () => void;
|
|
||||||
onNavigateToAdmin: () => void;
|
|
||||||
adminPassword?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomeScreen({ onStart, onNavigateToAdmin, adminPassword = '1234' }: HomeScreenProps) {
|
|
||||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
|
||||||
const [enteredPassword, setEnteredPassword] = useState('');
|
|
||||||
const [errorText, setErrorText] = useState('');
|
|
||||||
|
|
||||||
const handleSettingsTap = () => {
|
|
||||||
setEnteredPassword('');
|
|
||||||
setErrorText('');
|
|
||||||
setPasswordModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePasswordSubmit = () => {
|
|
||||||
if (enteredPassword === adminPassword) {
|
|
||||||
setPasswordModalVisible(false);
|
|
||||||
onNavigateToAdmin();
|
|
||||||
} else {
|
|
||||||
setErrorText('Incorrect Password');
|
|
||||||
setEnteredPassword('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableWithoutFeedback onPress={onStart}>
|
|
||||||
<View style={styles.container}>
|
|
||||||
{/* Settings button in the top-right corner */}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.settingsButton}
|
|
||||||
onPress={handleSettingsTap}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text style={styles.settingsIcon}>⚙️</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
<Text style={styles.logo}>SCHNAPPIX</Text>
|
|
||||||
<Text style={styles.title}>Welcome to our celebration!</Text>
|
|
||||||
|
|
||||||
<View style={styles.divider} />
|
|
||||||
|
|
||||||
<View style={styles.startButton}>
|
|
||||||
<Text style={styles.startButtonText}>TAP ANYWHERE TO START</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.footer}>Take photos • Create a collage • Print instantly</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Password Prompt Modal */}
|
|
||||||
<Modal
|
|
||||||
visible={passwordModalVisible}
|
|
||||||
transparent={true}
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={() => setPasswordModalVisible(false)}
|
|
||||||
>
|
|
||||||
<View style={styles.modalOverlay}>
|
|
||||||
<View style={styles.modalContent}>
|
|
||||||
<Text style={styles.modalTitle}>Admin Access</Text>
|
|
||||||
<Text style={styles.modalSub}>Enter password to open settings</Text>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
secureTextEntry
|
|
||||||
placeholder="Enter password"
|
|
||||||
placeholderTextColor={THEME.colors.textMuted}
|
|
||||||
value={enteredPassword}
|
|
||||||
onChangeText={setEnteredPassword}
|
|
||||||
keyboardType="numeric"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
{errorText ? <Text style={styles.error}>{errorText}</Text> : null}
|
|
||||||
|
|
||||||
<View style={styles.modalButtons}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, styles.cancelButton]}
|
|
||||||
onPress={() => setPasswordModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Text style={styles.buttonText}>Cancel</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, styles.confirmButton]}
|
|
||||||
onPress={handlePasswordSubmit}
|
|
||||||
>
|
|
||||||
<Text style={styles.buttonText}>Open</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: THEME.colors.background,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
settingsButton: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 24,
|
|
||||||
right: 24,
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
borderRadius: THEME.borderRadius.round,
|
|
||||||
backgroundColor: THEME.colors.surfaceSecondary,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: THEME.colors.border,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
zIndex: 999,
|
|
||||||
},
|
|
||||||
settingsIcon: {
|
|
||||||
fontSize: 24,
|
|
||||||
color: THEME.colors.text,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: THEME.spacing.xl,
|
|
||||||
borderRadius: THEME.borderRadius.lg,
|
|
||||||
backgroundColor: THEME.colors.surface,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: THEME.colors.border,
|
|
||||||
width: '60%',
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 10 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 20,
|
|
||||||
elevation: 10,
|
|
||||||
},
|
|
||||||
logo: {
|
|
||||||
fontSize: 54,
|
|
||||||
fontWeight: '900',
|
|
||||||
color: THEME.colors.primary,
|
|
||||||
letterSpacing: 8,
|
|
||||||
marginBottom: THEME.spacing.sm,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 22,
|
|
||||||
color: THEME.colors.text,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: THEME.spacing.lg,
|
|
||||||
letterSpacing: 1,
|
|
||||||
},
|
|
||||||
divider: {
|
|
||||||
width: 60,
|
|
||||||
height: 3,
|
|
||||||
backgroundColor: THEME.colors.accent,
|
|
||||||
marginBottom: THEME.spacing.xl,
|
|
||||||
borderRadius: THEME.borderRadius.sm,
|
|
||||||
},
|
|
||||||
startButton: {
|
|
||||||
paddingVertical: THEME.spacing.md,
|
|
||||||
paddingHorizontal: THEME.spacing.xl,
|
|
||||||
backgroundColor: THEME.colors.primary,
|
|
||||||
borderRadius: THEME.borderRadius.round,
|
|
||||||
marginBottom: THEME.spacing.xl,
|
|
||||||
},
|
|
||||||
startButtonText: {
|
|
||||||
color: THEME.colors.text,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
letterSpacing: 2,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: THEME.colors.textMuted,
|
|
||||||
letterSpacing: 1,
|
|
||||||
},
|
|
||||||
modalOverlay: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: THEME.colors.overlay,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
modalContent: {
|
|
||||||
width: 320,
|
|
||||||
padding: THEME.spacing.lg,
|
|
||||||
borderRadius: THEME.borderRadius.md,
|
|
||||||
backgroundColor: THEME.colors.surfaceSecondary,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: THEME.colors.border,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: THEME.colors.text,
|
|
||||||
marginBottom: THEME.spacing.xs,
|
|
||||||
},
|
|
||||||
modalSub: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: THEME.colors.textMuted,
|
|
||||||
marginBottom: THEME.spacing.md,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
width: '100%',
|
|
||||||
height: 50,
|
|
||||||
backgroundColor: THEME.colors.surface,
|
|
||||||
borderColor: THEME.colors.border,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: THEME.borderRadius.sm,
|
|
||||||
color: THEME.colors.text,
|
|
||||||
paddingHorizontal: THEME.spacing.md,
|
|
||||||
fontSize: 16,
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: THEME.spacing.sm,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
color: THEME.colors.error,
|
|
||||||
fontSize: 14,
|
|
||||||
marginBottom: THEME.spacing.sm,
|
|
||||||
},
|
|
||||||
modalButtons: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '100%',
|
|
||||||
marginTop: THEME.spacing.sm,
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
flex: 1,
|
|
||||||
height: 45,
|
|
||||||
borderRadius: THEME.borderRadius.sm,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginHorizontal: THEME.spacing.xs,
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: THEME.colors.border,
|
|
||||||
},
|
|
||||||
confirmButton: {
|
|
||||||
backgroundColor: THEME.colors.primary,
|
|
||||||
},
|
|
||||||
buttonText: {
|
|
||||||
color: THEME.colors.text,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import ViewShot from 'react-native-view-shot';
|
import ViewShot from 'react-native-view-shot';
|
||||||
|
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||||
import { THEME } from '../styles/theme';
|
import { THEME } from '../styles/theme';
|
||||||
import { printImageLocal } from '../services/printer';
|
import { printImageLocal } from '../services/printer';
|
||||||
import { saveToGallery } from '../services/storage';
|
import { saveToGallery } from '../services/storage';
|
||||||
@@ -41,36 +42,36 @@ export default function PreviewScreen({
|
|||||||
// Triggers the view capture and the print job
|
// Triggers the view capture and the print job
|
||||||
const handlePrint = async () => {
|
const handlePrint = async () => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setStatusMessage('Preparing your photo...');
|
setStatusMessage('Dein Foto wird vorbereitet...');
|
||||||
try {
|
try {
|
||||||
let printUri = currentPhoto;
|
let printUri = currentPhoto;
|
||||||
|
|
||||||
// If we are printing a collage layout, capture the ViewShot container
|
// If we are printing a collage layout, capture the ViewShot container
|
||||||
if (layout !== 'single') {
|
if (layout !== 'single') {
|
||||||
setStatusMessage('Generating collage...');
|
setStatusMessage('Collage wird generiert...');
|
||||||
const capturedUri = await viewShotRef.current.capture();
|
const capturedUri = await viewShotRef.current.capture();
|
||||||
printUri = capturedUri;
|
printUri = capturedUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusMessage('Sending print job to printer...');
|
setStatusMessage('Druckauftrag wird an Drucker gesendet...');
|
||||||
// 1. Silent Print via IPP
|
// 1. Silent Print via IPP
|
||||||
await printImageLocal(printUri, {
|
await printImageLocal(printUri, {
|
||||||
ipAddress: printerIp,
|
ipAddress: printerIp,
|
||||||
jobName: 'Schnappix Photo Booth',
|
jobName: 'Schnappix Fotobox',
|
||||||
});
|
});
|
||||||
|
|
||||||
setStatusMessage('Saving to tablet library...');
|
setStatusMessage('Wird in Galerie gespeichert...');
|
||||||
// 2. Save permanently to DCIM/Schnappix
|
// 2. Save permanently to DCIM/Schnappix
|
||||||
await saveToGallery(printUri);
|
await saveToGallery(printUri);
|
||||||
|
|
||||||
setStatusMessage('Print successful! Enjoy!');
|
setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
onReset(); // Go back to Home Screen
|
onReset(); // Go back to Home Screen
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Printing failed:', error);
|
console.error('Printing failed:', error);
|
||||||
alert('Error: ' + error.message);
|
alert('Fehler: ' + error.message);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -78,7 +79,7 @@ export default function PreviewScreen({
|
|||||||
// Saves to gallery without printing
|
// Saves to gallery without printing
|
||||||
const handleSaveOnly = async () => {
|
const handleSaveOnly = async () => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
setStatusMessage('Generating image...');
|
setStatusMessage('Bild wird generiert...');
|
||||||
try {
|
try {
|
||||||
let saveUri = currentPhoto;
|
let saveUri = currentPhoto;
|
||||||
|
|
||||||
@@ -87,21 +88,46 @@ export default function PreviewScreen({
|
|||||||
saveUri = capturedUri;
|
saveUri = capturedUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusMessage('Saving to tablet library...');
|
setStatusMessage('Wird in Galerie gespeichert...');
|
||||||
await saveToGallery(saveUri);
|
await saveToGallery(saveUri);
|
||||||
|
|
||||||
setStatusMessage('Saved successfully!');
|
setStatusMessage('Erfolgreich gespeichert!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
onReset();
|
onReset();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Saving failed:', error);
|
console.error('Saving failed:', error);
|
||||||
alert('Error: ' + error.message);
|
alert('Fehler: ' + error.message);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auto-save collage and exit
|
||||||
|
const handleExit = async () => {
|
||||||
|
// If layout is single, it has already been auto-saved to gallery right when captured
|
||||||
|
if (layout === 'single') {
|
||||||
|
onReset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setStatusMessage('Collage wird gespeichert...');
|
||||||
|
try {
|
||||||
|
const capturedUri = await viewShotRef.current.capture();
|
||||||
|
await saveToGallery(capturedUri);
|
||||||
|
setStatusMessage('Erfolgreich gespeichert!');
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsProcessing(false);
|
||||||
|
onReset();
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auto-saving collage on exit failed:', error);
|
||||||
|
setIsProcessing(false);
|
||||||
|
onReset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Render the selected collage inside the capture container
|
// Render the selected collage inside the capture container
|
||||||
const renderCollageView = () => {
|
const renderCollageView = () => {
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
@@ -114,8 +140,8 @@ export default function PreviewScreen({
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.collageFooter}>
|
<View style={styles.collageFooter}>
|
||||||
<Text style={styles.collageFooterText}>SCHNAPPIX PHOTO BOOTH</Text>
|
<Text style={styles.collageFooterText}>SCHNAPPIX FOTOBOX</Text>
|
||||||
<Text style={styles.collageFooterDate}>{new Date().toLocaleDateString()}</Text>
|
<Text style={styles.collageFooterDate}>{new Date().toLocaleDateString('de-DE')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -128,7 +154,7 @@ export default function PreviewScreen({
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.collageFooter}>
|
<View style={styles.collageFooter}>
|
||||||
<Text style={styles.collageFooterText}>SCHNAPPIX PHOTO BOOTH</Text>
|
<Text style={styles.collageFooterText}>SCHNAPPIX FOTOBOX</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -141,7 +167,7 @@ export default function PreviewScreen({
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.collageFooter}>
|
<View style={styles.collageFooter}>
|
||||||
<Text style={styles.collageFooterText}>SCHNAPPIX PHOTO BOOTH</Text>
|
<Text style={styles.collageFooterText}>SCHNAPPIX FOTOBOX</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -155,7 +181,7 @@ export default function PreviewScreen({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||||
{/* Left panel: Preview Canvas */}
|
{/* Left panel: Preview Canvas */}
|
||||||
<View style={styles.previewPanel}>
|
<View style={styles.previewPanel}>
|
||||||
{layout === 'single' ? (
|
{layout === 'single' ? (
|
||||||
@@ -174,7 +200,7 @@ export default function PreviewScreen({
|
|||||||
|
|
||||||
{/* Right panel: Controls */}
|
{/* Right panel: Controls */}
|
||||||
<View style={styles.controlPanel}>
|
<View style={styles.controlPanel}>
|
||||||
<Text style={styles.header}>CHOOSE YOUR STYLE</Text>
|
<Text style={styles.header}>WÄHLE DEINEN STIL</Text>
|
||||||
|
|
||||||
{/* Layout Selection */}
|
{/* Layout Selection */}
|
||||||
<View style={styles.layoutSelector}>
|
<View style={styles.layoutSelector}>
|
||||||
@@ -182,7 +208,7 @@ export default function PreviewScreen({
|
|||||||
style={[styles.layoutBtn, layout === 'single' && styles.layoutBtnActive]}
|
style={[styles.layoutBtn, layout === 'single' && styles.layoutBtnActive]}
|
||||||
onPress={() => setLayout('single')}
|
onPress={() => setLayout('single')}
|
||||||
>
|
>
|
||||||
<Text style={styles.layoutBtnText}>Single</Text>
|
<Text style={styles.layoutBtnText}>Einzelbild</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -206,7 +232,7 @@ export default function PreviewScreen({
|
|||||||
disabled={photoUris.length < 3}
|
disabled={photoUris.length < 3}
|
||||||
onPress={() => setLayout('strip')}
|
onPress={() => setLayout('strip')}
|
||||||
>
|
>
|
||||||
<Text style={styles.layoutBtnText}>Strip (3)</Text>
|
<Text style={styles.layoutBtnText}>Streifen (3)</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -218,37 +244,37 @@ export default function PreviewScreen({
|
|||||||
disabled={photoUris.length < 4}
|
disabled={photoUris.length < 4}
|
||||||
onPress={() => setLayout('grid')}
|
onPress={() => setLayout('grid')}
|
||||||
>
|
>
|
||||||
<Text style={styles.layoutBtnText}>Grid (4)</Text>
|
<Text style={styles.layoutBtnText}>Raster (4)</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.photosCountText}>
|
<Text style={styles.photosCountText}>
|
||||||
Photos Captured: {photoUris.length} / 4
|
Aufgenommene Fotos: {photoUris.length} / 4
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.printBtn]} onPress={handlePrint}>
|
<TouchableOpacity style={[styles.actionBtn, styles.printBtn]} onPress={handlePrint}>
|
||||||
<Text style={styles.actionBtnText}>PRINT NOW 🖨️</Text>
|
<Text style={styles.actionBtnText}>JETZT DRUCKEN 🖨️</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn]} onPress={handleSaveOnly}>
|
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn]} onPress={handleSaveOnly}>
|
||||||
<Text style={styles.actionBtnText}>Save to Tablet Only 💾</Text>
|
<Text style={styles.actionBtnText}>Nur auf Tablet speichern 💾</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{photoUris.length < 4 && (
|
{photoUris.length < 4 && (
|
||||||
<TouchableOpacity style={[styles.actionBtn, styles.addBtn]} onPress={onAddAnother}>
|
<TouchableOpacity style={[styles.actionBtn, styles.addBtn]} onPress={onAddAnother}>
|
||||||
<Text style={styles.actionBtnText}>+ Add Another Photo</Text>
|
<Text style={styles.actionBtnText}>+ Weiteres Foto aufnehmen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.rowActions}>
|
<View style={styles.rowActions}>
|
||||||
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn]} onPress={onRetakeLast}>
|
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn]} onPress={onRetakeLast}>
|
||||||
<Text style={styles.smallBtnText}>Retake Last</Text>
|
<Text style={styles.smallBtnText}>Letztes wiederholen</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn]} onPress={onReset}>
|
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn]} onPress={handleExit}>
|
||||||
<Text style={styles.smallBtnText}>Exit</Text>
|
<Text style={styles.smallBtnText}>Beenden</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -263,7 +289,7 @@ export default function PreviewScreen({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +307,6 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#050507',
|
backgroundColor: '#050507',
|
||||||
},
|
},
|
||||||
viewShotContainer: {
|
viewShotContainer: {
|
||||||
// 3:2 ratio aspect layout matching Canon Selphy postcard paper
|
|
||||||
width: 320,
|
width: 320,
|
||||||
height: 480,
|
height: 480,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -389,7 +414,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
// Collage Canvas Layouts (3:2 ratio aspect - width 320, height 480)
|
|
||||||
singleCanvas: {
|
singleCanvas: {
|
||||||
width: 320,
|
width: 320,
|
||||||
height: 480,
|
height: 480,
|
||||||
@@ -433,7 +457,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
color: '#666666',
|
color: '#666666',
|
||||||
},
|
},
|
||||||
// Strip (3 images vertical)
|
|
||||||
stripCanvas: {},
|
stripCanvas: {},
|
||||||
stripContent: {
|
stripContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -446,7 +469,6 @@ const styles = StyleSheet.create({
|
|||||||
resizeMode: 'cover',
|
resizeMode: 'cover',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
},
|
},
|
||||||
// Grid (4 images 2x2)
|
|
||||||
gridCanvas: {},
|
gridCanvas: {},
|
||||||
gridContainer: {
|
gridContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -462,7 +484,6 @@ const styles = StyleSheet.create({
|
|||||||
resizeMode: 'cover',
|
resizeMode: 'cover',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
},
|
},
|
||||||
// Duo (2 images side by side or stacked)
|
|
||||||
duoCanvas: {},
|
duoCanvas: {},
|
||||||
duoContainer: {
|
duoContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -475,7 +496,6 @@ const styles = StyleSheet.create({
|
|||||||
resizeMode: 'cover',
|
resizeMode: 'cover',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
},
|
},
|
||||||
// Loading overlay
|
|
||||||
loadingOverlay: {
|
loadingOverlay: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
+50
-32
@@ -1,6 +1,6 @@
|
|||||||
import ipp from 'ipp-encoder';
|
import ipp from 'ipp-encoder';
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import * as FileSystem from 'expo-file-system';
|
import { File } from 'expo-file-system';
|
||||||
|
|
||||||
export interface PrintOptions {
|
export interface PrintOptions {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
@@ -19,13 +19,12 @@ export async function printImageLocal(imageUri: string, options: PrintOptions):
|
|||||||
const { ipAddress, jobName = 'Schnappix Photo', username = 'Schnappix Kiosk' } = options;
|
const { ipAddress, jobName = 'Schnappix Photo', username = 'Schnappix Kiosk' } = options;
|
||||||
|
|
||||||
if (!ipAddress) {
|
if (!ipAddress) {
|
||||||
throw new Error('Printer IP address is required.');
|
throw new Error('Drucker-IP-Adresse ist erforderlich.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Read the image file from local storage as Base64 and convert to binary Buffer
|
// 1. Read the image file from local storage as Base64 and convert to binary Buffer
|
||||||
const base64Data = await FileSystem.readAsStringAsync(imageUri, {
|
const file = new File(imageUri);
|
||||||
encoding: FileSystem.EncodingType.Base64,
|
const base64Data = await file.base64();
|
||||||
});
|
|
||||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
// 2. Build the IPP Print-Job request structure
|
// 2. Build the IPP Print-Job request structure
|
||||||
@@ -57,36 +56,55 @@ export async function printImageLocal(imageUri: string, options: PrintOptions):
|
|||||||
// 5. Send the POST request to the printer via standard HTTP on port 631
|
// 5. Send the POST request to the printer via standard HTTP on port 631
|
||||||
const printerUrl = `http://${ipAddress}:631/ipp/print`;
|
const printerUrl = `http://${ipAddress}:631/ipp/print`;
|
||||||
|
|
||||||
console.log(`Sending silent print job to ${printerUrl}...`);
|
const maxRetries = 3;
|
||||||
|
let delay = 1000;
|
||||||
const response = await fetch(printerUrl, {
|
let lastError: any = null;
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/ipp',
|
|
||||||
},
|
|
||||||
// Pass as Uint8Array to ensure React Native fetch treats it as raw binary
|
|
||||||
body: new Uint8Array(finalPayload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
throw new Error(`Printer responded with HTTP status ${response.status}: ${response.statusText}`);
|
try {
|
||||||
}
|
console.log(`Sending silent print job to ${printerUrl} (Attempt ${attempt}/${maxRetries})...`);
|
||||||
|
|
||||||
|
const response = await fetch(printerUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/ipp',
|
||||||
|
},
|
||||||
|
// Pass as Uint8Array to ensure React Native fetch treats it as raw binary
|
||||||
|
body: new Uint8Array(finalPayload),
|
||||||
|
});
|
||||||
|
|
||||||
// 6. Read and decode the response from the printer to verify success
|
if (!response.ok) {
|
||||||
const responseArrayBuffer = await response.arrayBuffer();
|
throw new Error(`Drucker antwortete mit HTTP-Status ${response.status}: ${response.statusText}`);
|
||||||
const responseBuffer = Buffer.from(responseArrayBuffer);
|
}
|
||||||
|
|
||||||
try {
|
// 6. Read and decode the response from the printer to verify success
|
||||||
const decodedResponse = ipp.response.decode(responseBuffer);
|
const responseArrayBuffer = await response.arrayBuffer();
|
||||||
const statusCode = decodedResponse.statusCode;
|
const responseBuffer = Buffer.from(responseArrayBuffer);
|
||||||
|
|
||||||
// IPP success status is 0x0000 (successful-ok)
|
try {
|
||||||
if (statusCode !== 0x0000) {
|
const decodedResponse = ipp.response.decode(responseBuffer);
|
||||||
throw new Error(`Printer returned IPP error code: 0x${statusCode.toString(16)}`);
|
const statusCode = decodedResponse.statusCode;
|
||||||
|
|
||||||
|
// IPP success status is 0x0000 (successful-ok)
|
||||||
|
if (statusCode !== 0x0000) {
|
||||||
|
throw new Error(`Drucker lieferte IPP-Fehlercode: 0x${statusCode.toString(16)}`);
|
||||||
|
}
|
||||||
|
console.log('Silent print job accepted successfully!');
|
||||||
|
} catch (e: any) {
|
||||||
|
// If decoding fails, we still assume success since the network request completed successfully
|
||||||
|
console.warn('Failed to parse IPP response, but network request succeeded:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // Success, exit retry loop
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(`Print attempt ${attempt} failed: ${error.message}`);
|
||||||
|
lastError = error;
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
delay *= 2; // exponential backoff
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log('Silent print job accepted successfully!');
|
|
||||||
} catch (e: any) {
|
|
||||||
// If decoding fails, we still assume success since the network request completed successfully
|
|
||||||
console.warn('Failed to parse IPP response, but network request succeeded:', e.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error(`Druckauftrag nach ${maxRetries} Versuchen fehlgeschlagen. Letzter Fehler: ${lastError?.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as FileSystem from 'expo-file-system';
|
import { File, Paths } from 'expo-file-system';
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
countdownDuration: number; // in seconds
|
countdownDuration: number; // in seconds
|
||||||
@@ -7,7 +7,7 @@ export interface AppSettings {
|
|||||||
kioskModeEnabled: boolean;
|
kioskModeEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_FILE = `${FileSystem.documentDirectory}settings.json`;
|
const settingsFile = new File(Paths.document, 'settings.json');
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: AppSettings = {
|
const DEFAULT_SETTINGS: AppSettings = {
|
||||||
countdownDuration: 3,
|
countdownDuration: 3,
|
||||||
@@ -21,9 +21,8 @@ const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
*/
|
*/
|
||||||
export async function loadSettings(): Promise<AppSettings> {
|
export async function loadSettings(): Promise<AppSettings> {
|
||||||
try {
|
try {
|
||||||
const fileInfo = await FileSystem.getInfoAsync(SETTINGS_FILE);
|
if (settingsFile.exists) {
|
||||||
if (fileInfo.exists) {
|
const content = await settingsFile.text();
|
||||||
const content = await FileSystem.readAsStringAsync(SETTINGS_FILE);
|
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ export async function loadSettings(): Promise<AppSettings> {
|
|||||||
export async function saveSettings(settings: AppSettings): Promise<void> {
|
export async function saveSettings(settings: AppSettings): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const content = JSON.stringify(settings, null, 2);
|
const content = JSON.stringify(settings, null, 2);
|
||||||
await FileSystem.writeAsStringAsync(SETTINGS_FILE, content);
|
settingsFile.write(content);
|
||||||
console.log('Settings saved successfully:', content);
|
console.log('Settings saved successfully:', content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save settings:', e);
|
console.error('Failed to save settings:', e);
|
||||||
|
|||||||
+14
-10
@@ -1,5 +1,4 @@
|
|||||||
import * as MediaLibrary from 'expo-media-library';
|
import { Asset, Album, getPermissionsAsync, requestPermissionsAsync } from 'expo-media-library';
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
|
||||||
const ALBUM_NAME = 'Schnappix';
|
const ALBUM_NAME = 'Schnappix';
|
||||||
|
|
||||||
@@ -8,13 +7,13 @@ const ALBUM_NAME = 'Schnappix';
|
|||||||
* @returns Promise<boolean> True if permissions are granted.
|
* @returns Promise<boolean> True if permissions are granted.
|
||||||
*/
|
*/
|
||||||
export async function requestStoragePermission(): Promise<boolean> {
|
export async function requestStoragePermission(): Promise<boolean> {
|
||||||
const { status, canAskAgain } = await MediaLibrary.getPermissionsAsync();
|
const { status, canAskAgain } = await getPermissionsAsync();
|
||||||
if (status === 'granted') {
|
if (status === 'granted') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canAskAgain) {
|
if (canAskAgain) {
|
||||||
const { status: newStatus } = await MediaLibrary.requestPermissionsAsync();
|
const { status: newStatus } = await requestPermissionsAsync();
|
||||||
return newStatus === 'granted';
|
return newStatus === 'granted';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,25 +33,30 @@ export async function saveToGallery(localUri: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Create a media asset from the local file
|
// 1. Create a media asset from the local file
|
||||||
const asset = await MediaLibrary.createAssetAsync(localUri);
|
const asset = await Asset.create(localUri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. Check if the "Schnappix" album already exists
|
// 2. Check if the "Schnappix" album already exists
|
||||||
let album = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
|
const album = await Album.get(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
|
||||||
await MediaLibrary.createAlbumAsync(ALBUM_NAME, asset, false);
|
await Album.create(ALBUM_NAME, [asset], false);
|
||||||
console.log(`Created new album "${ALBUM_NAME}" and saved photo.`);
|
console.log(`Created new album "${ALBUM_NAME}" and saved photo.`);
|
||||||
} else {
|
} else {
|
||||||
// 4. If it exists, add our asset to it
|
// 4. If it exists, add our asset to it
|
||||||
await MediaLibrary.addAssetsToAlbumAsync([asset], album, false);
|
await album.add(asset);
|
||||||
console.log(`Saved photo to existing album "${ALBUM_NAME}".`);
|
console.log(`Saved photo to existing album "${ALBUM_NAME}".`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return asset.uri;
|
const assetUri = await asset.getUri();
|
||||||
|
return assetUri;
|
||||||
} 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, returning fallback asset URI:', e);
|
||||||
return asset.uri;
|
try {
|
||||||
|
return await asset.getUri();
|
||||||
|
} catch {
|
||||||
|
return asset.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
declare module 'ipp-encoder';
|
||||||
Reference in New Issue
Block a user