Update camera screens, configuration, and build setup
This commit is contained in:
@@ -39,3 +39,8 @@ yarn-error.*
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
|
||||
# Gradle/Android build folders
|
||||
**/build/
|
||||
**/.gradle/
|
||||
.vscode/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { StyleSheet, View, SafeAreaView, StatusBar } from 'react-native';
|
||||
import HomeScreen from './src/screens/HomeScreen';
|
||||
import CameraScreen from './src/screens/CameraScreen';
|
||||
import PreviewScreen from './src/screens/PreviewScreen';
|
||||
import AdminScreen from './src/screens/AdminScreen';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"slug": "Schnappix",
|
||||
"version": "1.0.0",
|
||||
"orientation": "landscape",
|
||||
"icon": "./assets/icon.png",
|
||||
"icon": "./Icon.png",
|
||||
"userInterfaceStyle": "dark",
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
@@ -14,7 +14,7 @@
|
||||
"package": "com.schnappix",
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#121214",
|
||||
"foregroundImage": "./assets/icon.png"
|
||||
"foregroundImage": "./Icon.png"
|
||||
},
|
||||
"permissions": [
|
||||
"android.permission.CAMERA",
|
||||
@@ -48,15 +48,14 @@
|
||||
{
|
||||
"android": {
|
||||
"extraMavenRepos": [
|
||||
"https://jitpack.io",
|
||||
"https://jcenter.bintray.com"
|
||||
"https://jitpack.io"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"./plugins/withKioskAdmin",
|
||||
"./plugins/withUsbCamera",
|
||||
"./plugins/withSettingsRepositories"
|
||||
"./plugins/withLocalAars"
|
||||
],
|
||||
"extra": {
|
||||
"eas": {
|
||||
|
||||
@@ -31,7 +31,7 @@ dependencies {
|
||||
exclude group: 'com.gyf.immersionbar', module: 'immersionbar'
|
||||
exclude group: 'com.zlc.glide', module: 'webpdecoder'
|
||||
}
|
||||
// Local AAR dependencies downloaded to avoid JCenter resolution issues
|
||||
implementation files('libs/immersionbar-3.0.0.aar')
|
||||
implementation files('libs/webpdecoder-1.6.4.9.0.aar')
|
||||
// Local AAR dependencies compiled only; injected into app build.gradle by plugin
|
||||
compileOnly files('libs/immersionbar-3.0.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;
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Switch,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
import { THEME } from '../styles/theme';
|
||||
import KioskMode from '../../modules/kiosk-mode';
|
||||
import { AppSettings, saveSettings } from '../services/settings';
|
||||
@@ -82,10 +83,16 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
return;
|
||||
}
|
||||
|
||||
if (!printerIp.trim()) {
|
||||
const ipTrimmed = printerIp.trim();
|
||||
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;
|
||||
}
|
||||
|
||||
if (!password.trim() || password.length < 4) {
|
||||
Alert.alert('Ungültige Eingabe', 'Das Admin-Passwort muss mindestens 4 Ziffern lang sein.');
|
||||
@@ -94,7 +101,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
|
||||
const updated: AppSettings = {
|
||||
countdownDuration: duration,
|
||||
printerIp: printerIp.trim(),
|
||||
printerIp: ipTrimmed,
|
||||
adminPassword: password.trim(),
|
||||
kioskModeEnabled: kioskActive,
|
||||
};
|
||||
@@ -109,7 +116,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.title}>SCHNAPPIX EINSTELLUNGEN</Text>
|
||||
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
|
||||
@@ -207,7 +214,7 @@ export default function AdminScreen({ currentSettings, onSave, onClose }: AdminS
|
||||
<Text style={styles.saveBtnText}>EINSTELLUNGEN SPEICHERN</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
TouchableWithoutFeedback,
|
||||
} from 'react-native';
|
||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
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 { UsbCameraView, UsbCameraRef } from '../../modules/usb-camera';
|
||||
import { saveToGallery } from '../services/storage';
|
||||
@@ -48,6 +49,22 @@ export default function CameraScreen({
|
||||
const usbCameraRef = useRef<UsbCameraRef>(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)
|
||||
useEffect(() => {
|
||||
if (!permission || !permission.granted) {
|
||||
@@ -117,7 +134,8 @@ export default function CameraScreen({
|
||||
// 4. Capture photo function
|
||||
const capture = async () => {
|
||||
const filename = `photo_${Date.now()}.jpg`;
|
||||
const tempUri = `${FileSystem.cacheDirectory}${filename}`;
|
||||
const tempFile = new File(Paths.cache, filename);
|
||||
const tempUri = tempFile.uri;
|
||||
|
||||
try {
|
||||
if (Platform.OS === 'android' && isUsbConnected && usbCameraRef.current) {
|
||||
@@ -289,9 +307,9 @@ export default function CameraScreen({
|
||||
{/* Countdown overlay */}
|
||||
{hasStarted && (
|
||||
<View style={styles.countdownOverlay} pointerEvents="none">
|
||||
<View style={styles.countdownBox}>
|
||||
<Animated.View style={[styles.countdownBox, animatedStyle]}>
|
||||
<Text style={styles.countdownText}>{countdown}</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -306,7 +324,7 @@ export default function CameraScreen({
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||
{/* Camera Preview Background */}
|
||||
<View style={styles.previewContainer}>
|
||||
{Platform.OS === 'android' && isUsbConnected ? (
|
||||
@@ -322,7 +340,7 @@ export default function CameraScreen({
|
||||
|
||||
{/* Render UI controls/overlays on top */}
|
||||
{renderContent()}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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('Falsches Passwort');
|
||||
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}>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.footer}>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 eingeben"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
} from 'react-native';
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
import { THEME } from '../styles/theme';
|
||||
import { printImageLocal } from '../services/printer';
|
||||
import { saveToGallery } from '../services/storage';
|
||||
@@ -180,7 +181,7 @@ export default function PreviewScreen({
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||
{/* Left panel: Preview Canvas */}
|
||||
<View style={styles.previewPanel}>
|
||||
{layout === 'single' ? (
|
||||
@@ -288,7 +289,7 @@ export default function PreviewScreen({
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+23
-5
@@ -1,6 +1,6 @@
|
||||
import ipp from 'ipp-encoder';
|
||||
import { Buffer } from 'buffer';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import { File } from 'expo-file-system';
|
||||
|
||||
export interface PrintOptions {
|
||||
ipAddress: string;
|
||||
@@ -23,9 +23,8 @@ export async function printImageLocal(imageUri: string, options: PrintOptions):
|
||||
}
|
||||
|
||||
// 1. Read the image file from local storage as Base64 and convert to binary Buffer
|
||||
const base64Data = await FileSystem.readAsStringAsync(imageUri, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
const file = new File(imageUri);
|
||||
const base64Data = await file.base64();
|
||||
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// 2. Build the IPP Print-Job request structure
|
||||
@@ -57,7 +56,13 @@ export async function printImageLocal(imageUri: string, options: PrintOptions):
|
||||
// 5. Send the POST request to the printer via standard HTTP on port 631
|
||||
const printerUrl = `http://${ipAddress}:631/ipp/print`;
|
||||
|
||||
console.log(`Sending silent print job to ${printerUrl}...`);
|
||||
const maxRetries = 3;
|
||||
let delay = 1000;
|
||||
let lastError: any = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
console.log(`Sending silent print job to ${printerUrl} (Attempt ${attempt}/${maxRetries})...`);
|
||||
|
||||
const response = await fetch(printerUrl, {
|
||||
method: 'POST',
|
||||
@@ -89,4 +94,17 @@ export async function printImageLocal(imageUri: string, options: PrintOptions):
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Druckauftrag nach ${maxRetries} Versuchen fehlgeschlagen. Letzter Fehler: ${lastError?.message}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import { File, Paths } from 'expo-file-system';
|
||||
|
||||
export interface AppSettings {
|
||||
countdownDuration: number; // in seconds
|
||||
@@ -7,7 +7,7 @@ export interface AppSettings {
|
||||
kioskModeEnabled: boolean;
|
||||
}
|
||||
|
||||
const SETTINGS_FILE = `${FileSystem.documentDirectory}settings.json`;
|
||||
const settingsFile = new File(Paths.document, 'settings.json');
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = {
|
||||
countdownDuration: 3,
|
||||
@@ -21,9 +21,8 @@ const DEFAULT_SETTINGS: AppSettings = {
|
||||
*/
|
||||
export async function loadSettings(): Promise<AppSettings> {
|
||||
try {
|
||||
const fileInfo = await FileSystem.getInfoAsync(SETTINGS_FILE);
|
||||
if (fileInfo.exists) {
|
||||
const content = await FileSystem.readAsStringAsync(SETTINGS_FILE);
|
||||
if (settingsFile.exists) {
|
||||
const content = await settingsFile.text();
|
||||
const parsed = JSON.parse(content);
|
||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||
}
|
||||
@@ -39,7 +38,7 @@ export async function loadSettings(): Promise<AppSettings> {
|
||||
export async function saveSettings(settings: AppSettings): Promise<void> {
|
||||
try {
|
||||
const content = JSON.stringify(settings, null, 2);
|
||||
await FileSystem.writeAsStringAsync(SETTINGS_FILE, content);
|
||||
settingsFile.write(content);
|
||||
console.log('Settings saved successfully:', content);
|
||||
} catch (e) {
|
||||
console.error('Failed to save settings:', e);
|
||||
|
||||
+14
-10
@@ -1,5 +1,4 @@
|
||||
import * as MediaLibrary from 'expo-media-library/legacy';
|
||||
import { Platform } from 'react-native';
|
||||
import { Asset, Album, getPermissionsAsync, requestPermissionsAsync } from 'expo-media-library';
|
||||
|
||||
const ALBUM_NAME = 'Schnappix';
|
||||
|
||||
@@ -8,13 +7,13 @@ const ALBUM_NAME = 'Schnappix';
|
||||
* @returns Promise<boolean> True if permissions are granted.
|
||||
*/
|
||||
export async function requestStoragePermission(): Promise<boolean> {
|
||||
const { status, canAskAgain } = await MediaLibrary.getPermissionsAsync();
|
||||
const { status, canAskAgain } = await getPermissionsAsync();
|
||||
if (status === 'granted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (canAskAgain) {
|
||||
const { status: newStatus } = await MediaLibrary.requestPermissionsAsync();
|
||||
const { status: newStatus } = await requestPermissionsAsync();
|
||||
return newStatus === 'granted';
|
||||
}
|
||||
|
||||
@@ -34,25 +33,30 @@ export async function saveToGallery(localUri: string): Promise<string> {
|
||||
}
|
||||
|
||||
// 1. Create a media asset from the local file
|
||||
const asset = await MediaLibrary.createAssetAsync(localUri);
|
||||
const asset = await Asset.create(localUri);
|
||||
|
||||
try {
|
||||
// 2. Check if the "Schnappix" album already exists
|
||||
let album = await MediaLibrary.getAlbumAsync(ALBUM_NAME);
|
||||
const album = await Album.get(ALBUM_NAME);
|
||||
|
||||
if (!album) {
|
||||
// 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.`);
|
||||
} else {
|
||||
// 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}".`);
|
||||
}
|
||||
|
||||
return asset.uri;
|
||||
const assetUri = await asset.getUri();
|
||||
return assetUri;
|
||||
} catch (e: any) {
|
||||
console.error('Error saving asset to album, returning fallback asset URI:', e);
|
||||
return asset.uri;
|
||||
try {
|
||||
return await asset.getUri();
|
||||
} catch {
|
||||
return asset.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user