Fix iOS crash, storage, and printing issues; setup apk build
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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%',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user