Fix iOS crash, storage, and printing issues; setup apk build

This commit is contained in:
2026-05-30 23:09:36 +02:00
parent eee684a3c9
commit 54666b077a
19 changed files with 1821 additions and 149 deletions
+196
View File
@@ -0,0 +1,196 @@
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
runOnJS,
} from 'react-native-reanimated';
import { THEME } from '../styles/theme';
export interface StickerData {
id: string;
type: 'emoji' | 'text';
content: string;
x: number;
y: number;
scale: number;
rotation: number;
}
interface DraggableStickerProps {
sticker: StickerData;
isSelected: boolean;
onSelect: (id: string) => void;
onDelete: (id: string) => void;
onUpdate: (id: string, updates: Partial<StickerData>) => void;
}
export default function DraggableSticker({
sticker,
isSelected,
onSelect,
onDelete,
onUpdate,
}: DraggableStickerProps) {
const translateX = useSharedValue(sticker.x);
const translateY = useSharedValue(sticker.y);
const scale = useSharedValue(sticker.scale);
const rotation = useSharedValue(sticker.rotation);
const savedTranslateX = useSharedValue(sticker.x);
const savedTranslateY = useSharedValue(sticker.y);
const savedScale = useSharedValue(sticker.scale);
const savedRotation = useSharedValue(sticker.rotation);
const selectSticker = () => {
onSelect(sticker.id);
};
const updateSticker = (x: number, y: number, s: number, r: number) => {
onUpdate(sticker.id, { x, y, scale: s, rotation: r });
};
const panGesture = Gesture.Pan()
.onStart(() => {
savedTranslateX.value = translateX.value;
savedTranslateY.value = translateY.value;
runOnJS(selectSticker)();
})
.onUpdate((event) => {
translateX.value = savedTranslateX.value + event.translationX;
translateY.value = savedTranslateY.value + event.translationY;
})
.onEnd(() => {
runOnJS(updateSticker)(
translateX.value,
translateY.value,
scale.value,
rotation.value
);
})
.minDistance(5);
const pinchGesture = Gesture.Pinch()
.onStart(() => {
savedScale.value = scale.value;
})
.onUpdate((event) => {
const newScale = savedScale.value * event.scale;
scale.value = Math.max(0.3, Math.min(4, newScale));
})
.onEnd(() => {
runOnJS(updateSticker)(
translateX.value,
translateY.value,
scale.value,
rotation.value
);
});
const rotationGesture = Gesture.Rotation()
.onStart(() => {
savedRotation.value = rotation.value;
})
.onUpdate((event) => {
rotation.value = savedRotation.value + event.rotation;
})
.onEnd(() => {
runOnJS(updateSticker)(
translateX.value,
translateY.value,
scale.value,
rotation.value
);
});
const tapGesture = Gesture.Tap()
.onEnd(() => {
runOnJS(selectSticker)();
});
const composedDrag = Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture);
const gesture = Gesture.Exclusive(composedDrag, tapGesture);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
{ rotate: `${rotation.value}rad` },
],
}));
const isText = sticker.type === 'text';
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.stickerWrapper, animatedStyle]}>
{isText ? (
<View style={styles.textStickerBox}>
<Text style={styles.textStickerContent}>{sticker.content}</Text>
</View>
) : (
<Text style={styles.emojiText}>{sticker.content}</Text>
)}
{isSelected && (
<TouchableOpacity
style={styles.deleteBtn}
onPress={() => onDelete(sticker.id)}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
<Text style={styles.deleteBtnText}>×</Text>
</TouchableOpacity>
)}
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
stickerWrapper: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
zIndex: 100,
},
emojiText: {
fontSize: 48,
textAlign: 'center',
},
textStickerBox: {
backgroundColor: 'rgba(5, 5, 16, 0.7)',
paddingVertical: 4,
paddingHorizontal: 10,
borderRadius: 6,
borderWidth: 1,
borderColor: THEME.colors.accent,
},
textStickerContent: {
fontSize: 14,
fontWeight: 'bold',
color: THEME.colors.text,
textAlign: 'center',
textShadowColor: THEME.colors.cyanGlow,
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 6,
},
deleteBtn: {
position: 'absolute',
top: -10,
right: -10,
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: THEME.colors.error,
alignItems: 'center',
justifyContent: 'center',
zIndex: 200,
},
deleteBtnText: {
color: '#fff',
fontSize: 14,
fontWeight: 'bold',
lineHeight: 16,
},
});
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import { StyleSheet, Image, View } from 'react-native';
interface FrameOverlayProps {
frameAsset: any; // require() asset
}
/**
* Renders a frame PNG overlay on top of the photo.
* The frame PNG must have a transparent center (alpha=0) so the photo shows through.
* Positioned absolutely to cover the entire photo canvas.
*/
export default function FrameOverlay({ frameAsset }: FrameOverlayProps) {
if (!frameAsset) return null;
return (
<View style={styles.container} pointerEvents="none">
<Image
source={frameAsset}
style={styles.frameImage}
resizeMode="cover"
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 50,
},
frameImage: {
width: '100%',
height: '100%',
},
});
+137
View File
@@ -0,0 +1,137 @@
import React from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
ScrollView,
Image,
} from 'react-native';
import { FRAMES, FrameDefinition } from '../data/frames';
import { THEME } from '../styles/theme';
interface FramePickerProps {
selectedFrameId: string | null;
onSelectFrame: (frameId: string | null) => void;
}
/**
* Horizontal scrollable frame picker.
* Shows "Kein Rahmen" (no frame) + all available frames from the registry.
*/
export default function FramePicker({ selectedFrameId, onSelectFrame }: FramePickerProps) {
if (FRAMES.length === 0) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>Keine Rahmen verfügbar</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Rahmen wählen</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
{/* No frame option */}
<TouchableOpacity
style={[
styles.frameThumb,
selectedFrameId === null && styles.frameThumbActive,
]}
onPress={() => onSelectFrame(null)}
>
<View style={styles.noFrameBox}>
<Text style={styles.noFrameText}></Text>
</View>
<Text style={styles.frameName}>Kein Rahmen</Text>
</TouchableOpacity>
{/* Frame options */}
{FRAMES.map((frame: FrameDefinition) => (
<TouchableOpacity
key={frame.id}
style={[
styles.frameThumb,
selectedFrameId === frame.id && styles.frameThumbActive,
]}
onPress={() => onSelectFrame(frame.id)}
>
<Image
source={frame.asset}
style={styles.thumbImage}
resizeMode="contain"
/>
<Text style={styles.frameName}>{frame.name}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: THEME.spacing.md,
},
emptyContainer: {
padding: THEME.spacing.md,
alignItems: 'center',
},
emptyText: {
color: THEME.colors.textMuted,
fontSize: 13,
fontStyle: 'italic',
},
title: {
fontSize: 13,
fontWeight: 'bold',
color: THEME.colors.textMuted,
letterSpacing: 1,
textTransform: 'uppercase',
marginBottom: THEME.spacing.sm,
},
scrollContent: {
gap: 8,
},
frameThumb: {
width: 64,
alignItems: 'center',
padding: 4,
borderRadius: THEME.borderRadius.sm,
borderWidth: 1,
borderColor: 'transparent',
},
frameThumbActive: {
borderColor: THEME.colors.primary,
backgroundColor: 'rgba(255, 43, 214, 0.1)',
},
noFrameBox: {
width: 48,
height: 64,
backgroundColor: THEME.colors.surfaceSecondary,
borderRadius: 4,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: THEME.colors.border,
},
noFrameText: {
fontSize: 20,
color: THEME.colors.textMuted,
},
thumbImage: {
width: 48,
height: 64,
borderRadius: 4,
},
frameName: {
fontSize: 10,
color: THEME.colors.textMuted,
marginTop: 3,
textAlign: 'center',
},
});
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import { THEME } from '../styles/theme';
interface IconProps {
icon: IconDefinition;
size?: number;
color?: string;
style?: any;
}
/**
* Schnappix Icon wrapper around FontAwesome.
* Defaults to the neon accent cyan color.
*/
export default function Icon({ icon, size = 24, color, style }: IconProps) {
return (
<FontAwesomeIcon
icon={icon}
size={size}
color={color || THEME.colors.accent}
style={style}
/>
);
}
+160
View File
@@ -0,0 +1,160 @@
import React from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
ScrollView,
} from 'react-native';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import { faCalendarDay, faClock, faChampagneGlasses, faXmark } from '@fortawesome/free-solid-svg-icons';
import { THEME } from '../styles/theme';
// Curated party emoji set (Mix of A + D per user request)
const PARTY_EMOJIS = [
'🎉', '🥳', '🎈', '🍾', '🥂', '💃', '🕺', '👑',
'🌟', '💖', '🔥', '🎶', '🍻', '🎭', '🤩', '😎',
'🤪', '😍', '🤗', '✨', '💫', '⭐', '🎊', '🎀',
'🦄', '🌈', '🎵', '🎤', '💎', '🎁',
];
interface StickerTrayProps {
onAddEmoji: (emoji: string) => void;
onAddDateSticker: (format: 'date' | 'datetime') => void;
onAddEventSticker: () => void;
onClose: () => void;
eventText: string;
}
export default function StickerTray({
onAddEmoji,
onAddDateSticker,
onAddEventSticker,
onClose,
eventText,
}: StickerTrayProps) {
return (
<View style={styles.container}>
{/* Header row */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Sticker hinzufügen</Text>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<FontAwesomeIcon icon={faXmark} size={18} color={THEME.colors.text} />
</TouchableOpacity>
</View>
{/* Date/Time quick-add buttons */}
<View style={styles.dateRow}>
<TouchableOpacity
style={styles.dateBtn}
onPress={() => onAddDateSticker('date')}
>
<FontAwesomeIcon icon={faCalendarDay} size={14} color={THEME.colors.accent} />
<Text style={styles.dateBtnText}>Datum</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.dateBtn}
onPress={() => onAddDateSticker('datetime')}
>
<FontAwesomeIcon icon={faClock} size={14} color={THEME.colors.accent} />
<Text style={styles.dateBtnText}>Datum + Uhrzeit</Text>
</TouchableOpacity>
{eventText ? (
<TouchableOpacity style={styles.dateBtn} onPress={onAddEventSticker}>
<FontAwesomeIcon icon={faChampagneGlasses} size={14} color={THEME.colors.primary} />
<Text style={[styles.dateBtnText, { color: THEME.colors.primary }]}>
{eventText.length > 15 ? eventText.substring(0, 15) + '…' : eventText}
</Text>
</TouchableOpacity>
) : null}
</View>
{/* Emoji grid */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.emojiScroll}
>
{PARTY_EMOJIS.map((emoji, idx) => (
<TouchableOpacity
key={idx}
style={styles.emojiBtn}
onPress={() => onAddEmoji(emoji)}
activeOpacity={0.6}
>
<Text style={styles.emojiText}>{emoji}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(13, 13, 26, 0.95)',
borderTopWidth: 1,
borderTopColor: THEME.colors.border,
paddingBottom: 8,
zIndex: 500,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 6,
},
headerTitle: {
fontSize: 13,
fontWeight: 'bold',
color: THEME.colors.textMuted,
letterSpacing: 1,
textTransform: 'uppercase',
},
closeBtn: {
padding: 4,
},
dateRow: {
flexDirection: 'row',
paddingHorizontal: 12,
paddingBottom: 6,
gap: 8,
},
dateBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
backgroundColor: THEME.colors.surfaceSecondary,
paddingVertical: 5,
paddingHorizontal: 10,
borderRadius: THEME.borderRadius.sm,
borderWidth: 1,
borderColor: THEME.colors.border,
},
dateBtnText: {
fontSize: 12,
color: THEME.colors.accent,
fontWeight: '600',
},
emojiScroll: {
paddingHorizontal: 8,
paddingVertical: 4,
},
emojiBtn: {
width: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 2,
borderRadius: THEME.borderRadius.sm,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
},
emojiText: {
fontSize: 28,
},
});