Fix iOS crash, storage, and printing issues; setup apk build
This commit is contained in:
+324
-51
@@ -1,18 +1,32 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import ViewShot from 'react-native-view-shot';
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
|
||||
import {
|
||||
faPrint,
|
||||
faDownload,
|
||||
faPlus,
|
||||
faRotateLeft,
|
||||
faRightFromBracket,
|
||||
faFaceGrinStars,
|
||||
faBorderAll,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { THEME } from '../styles/theme';
|
||||
import { printImageLocal } from '../services/printer';
|
||||
import { saveToGallery } from '../services/storage';
|
||||
import DraggableSticker, { StickerData } from '../components/DraggableSticker';
|
||||
import StickerTray from '../components/StickerTray';
|
||||
import FrameOverlay from '../components/FrameOverlay';
|
||||
import FramePicker from '../components/FramePicker';
|
||||
import { FRAMES } from '../data/frames';
|
||||
|
||||
interface PreviewScreenProps {
|
||||
photoUris: string[];
|
||||
@@ -20,6 +34,14 @@ interface PreviewScreenProps {
|
||||
onRetakeLast: () => void;
|
||||
onAddAnother: () => void;
|
||||
onReset: () => void;
|
||||
// Frame settings
|
||||
frameMode: 'off' | 'always' | 'available';
|
||||
initialFrameId: string | null;
|
||||
// Date overlay settings
|
||||
dateOverlay: 'off' | 'date' | 'datetime';
|
||||
dateOverlayPosition: string;
|
||||
// Event text
|
||||
eventText: string;
|
||||
}
|
||||
|
||||
type CollageLayout = 'single' | 'strip' | 'grid' | 'duo';
|
||||
@@ -30,44 +52,147 @@ export default function PreviewScreen({
|
||||
onRetakeLast,
|
||||
onAddAnother,
|
||||
onReset,
|
||||
frameMode,
|
||||
initialFrameId,
|
||||
dateOverlay,
|
||||
dateOverlayPosition,
|
||||
eventText,
|
||||
}: PreviewScreenProps) {
|
||||
const [layout, setLayout] = useState<CollageLayout>('single');
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
|
||||
|
||||
// Sticker state
|
||||
const [stickers, setStickers] = useState<StickerData[]>([]);
|
||||
const [selectedStickerId, setSelectedStickerId] = useState<string | null>(null);
|
||||
const [showStickerTray, setShowStickerTray] = useState<boolean>(false);
|
||||
|
||||
// Frame state
|
||||
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(
|
||||
frameMode === 'always' ? initialFrameId : null
|
||||
);
|
||||
|
||||
const viewShotRef = useRef<any>(null);
|
||||
|
||||
const currentPhoto = photoUris[photoUris.length - 1];
|
||||
|
||||
// Get the active frame asset
|
||||
const activeFrame = selectedFrameId
|
||||
? FRAMES.find((f) => f.id === selectedFrameId)
|
||||
: null;
|
||||
|
||||
// Format date/time for overlays and stickers
|
||||
const formatDate = useCallback((mode: 'date' | 'datetime'): string => {
|
||||
const now = new Date();
|
||||
const date = now.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
if (mode === 'datetime') {
|
||||
const time = now.toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
return `${date} ${time}`;
|
||||
}
|
||||
return date;
|
||||
}, []);
|
||||
|
||||
// Get date overlay position styles
|
||||
const getDatePositionStyle = () => {
|
||||
switch (dateOverlayPosition) {
|
||||
case 'bottom-left':
|
||||
return { bottom: 12, left: 12 };
|
||||
case 'top-right':
|
||||
return { top: 12, right: 12 };
|
||||
case 'top-left':
|
||||
return { top: 12, left: 12 };
|
||||
case 'bottom-center':
|
||||
return { bottom: 12, left: 0, right: 0, alignItems: 'center' as const };
|
||||
case 'bottom-right':
|
||||
default:
|
||||
return { bottom: 12, right: 12 };
|
||||
}
|
||||
};
|
||||
|
||||
// ── Sticker handlers ──
|
||||
const addSticker = useCallback((type: 'emoji' | 'text', content: string) => {
|
||||
const newSticker: StickerData = {
|
||||
id: `sticker_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
||||
type,
|
||||
content,
|
||||
x: 100 + Math.random() * 40 - 20,
|
||||
y: 100 + Math.random() * 40 - 20,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
};
|
||||
setStickers((prev) => [...prev, newSticker]);
|
||||
setSelectedStickerId(newSticker.id);
|
||||
}, []);
|
||||
|
||||
const handleAddEmoji = useCallback((emoji: string) => {
|
||||
addSticker('emoji', emoji);
|
||||
}, [addSticker]);
|
||||
|
||||
const handleAddDateSticker = useCallback((format: 'date' | 'datetime') => {
|
||||
addSticker('text', formatDate(format));
|
||||
}, [addSticker, formatDate]);
|
||||
|
||||
const handleAddEventSticker = useCallback(() => {
|
||||
const date = formatDate('date');
|
||||
addSticker('text', `${eventText} • ${date}`);
|
||||
}, [addSticker, formatDate, eventText]);
|
||||
|
||||
const handleStickerSelect = useCallback((id: string) => {
|
||||
setSelectedStickerId((prev) => (prev === id ? null : id));
|
||||
}, []);
|
||||
|
||||
const handleStickerDelete = useCallback((id: string) => {
|
||||
setStickers((prev) => prev.filter((s) => s.id !== id));
|
||||
setSelectedStickerId(null);
|
||||
}, []);
|
||||
|
||||
const handleStickerUpdate = useCallback((id: string, updates: Partial<StickerData>) => {
|
||||
setStickers((prev) =>
|
||||
prev.map((s) => (s.id === id ? { ...s, ...updates } : s))
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Deselect stickers when tapping the photo area
|
||||
const handleCanvasPress = useCallback(() => {
|
||||
setSelectedStickerId(null);
|
||||
}, []);
|
||||
|
||||
// ── Capture the final image ──
|
||||
const captureComposite = async (): Promise<string> => {
|
||||
if (viewShotRef.current) {
|
||||
return await viewShotRef.current.capture();
|
||||
}
|
||||
return currentPhoto;
|
||||
};
|
||||
|
||||
// Triggers the view capture and the print job
|
||||
const handlePrint = async () => {
|
||||
setIsProcessing(true);
|
||||
setStatusMessage('Dein Foto wird vorbereitet...');
|
||||
try {
|
||||
let printUri = currentPhoto;
|
||||
|
||||
// If we are printing a collage layout, capture the ViewShot container
|
||||
if (layout !== 'single') {
|
||||
setStatusMessage('Collage wird generiert...');
|
||||
const capturedUri = await viewShotRef.current.capture();
|
||||
printUri = capturedUri;
|
||||
}
|
||||
setStatusMessage('Bild wird generiert...');
|
||||
const printUri = await captureComposite();
|
||||
|
||||
setStatusMessage('Druckauftrag wird an Drucker gesendet...');
|
||||
// 1. Silent Print via IPP
|
||||
await printImageLocal(printUri, {
|
||||
ipAddress: printerIp,
|
||||
jobName: 'Schnappix Fotobox',
|
||||
});
|
||||
|
||||
setStatusMessage('Wird in Galerie gespeichert...');
|
||||
// 2. Save permanently to DCIM/Schnappix
|
||||
await saveToGallery(printUri);
|
||||
|
||||
setStatusMessage('Druck erfolgreich! Viel Spaß! 🎉');
|
||||
setTimeout(() => {
|
||||
setIsProcessing(false);
|
||||
onReset(); // Go back to Home Screen
|
||||
onReset();
|
||||
}, 2000);
|
||||
} catch (error: any) {
|
||||
console.error('Printing failed:', error);
|
||||
@@ -81,12 +206,7 @@ export default function PreviewScreen({
|
||||
setIsProcessing(true);
|
||||
setStatusMessage('Bild wird generiert...');
|
||||
try {
|
||||
let saveUri = currentPhoto;
|
||||
|
||||
if (layout !== 'single') {
|
||||
const capturedUri = await viewShotRef.current.capture();
|
||||
saveUri = capturedUri;
|
||||
}
|
||||
const saveUri = await captureComposite();
|
||||
|
||||
setStatusMessage('Wird in Galerie gespeichert...');
|
||||
await saveToGallery(saveUri);
|
||||
@@ -103,18 +223,17 @@ export default function PreviewScreen({
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-save collage and exit
|
||||
// Auto-save and exit
|
||||
const handleExit = async () => {
|
||||
// If layout is single, it has already been auto-saved to gallery right when captured
|
||||
if (layout === 'single') {
|
||||
if (stickers.length === 0 && layout === 'single' && !activeFrame && dateOverlay === 'off') {
|
||||
onReset();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setStatusMessage('Collage wird gespeichert...');
|
||||
setStatusMessage('Wird gespeichert...');
|
||||
try {
|
||||
const capturedUri = await viewShotRef.current.capture();
|
||||
const capturedUri = await captureComposite();
|
||||
await saveToGallery(capturedUri);
|
||||
setStatusMessage('Erfolgreich gespeichert!');
|
||||
setTimeout(() => {
|
||||
@@ -122,13 +241,13 @@ export default function PreviewScreen({
|
||||
onReset();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Auto-saving collage on exit failed:', error);
|
||||
console.error('Auto-saving on exit failed:', error);
|
||||
setIsProcessing(false);
|
||||
onReset();
|
||||
}
|
||||
};
|
||||
|
||||
// Render the selected collage inside the capture container
|
||||
// ── Render the collage ──
|
||||
const renderCollageView = () => {
|
||||
switch (layout) {
|
||||
case 'strip':
|
||||
@@ -183,20 +302,54 @@ export default function PreviewScreen({
|
||||
return (
|
||||
<Animated.View entering={FadeIn.duration(400)} exiting={FadeOut.duration(300)} style={styles.container}>
|
||||
{/* Left panel: Preview Canvas */}
|
||||
<View style={styles.previewPanel}>
|
||||
{layout === 'single' ? (
|
||||
renderCollageView()
|
||||
) : (
|
||||
// ViewShot wraps the collage to capture it at exactly 3:2 ratio (1200x1800 px) for the printer
|
||||
<ViewShot
|
||||
ref={viewShotRef}
|
||||
options={{ format: 'jpg', quality: 0.95 }}
|
||||
style={styles.viewShotContainer}
|
||||
>
|
||||
{renderCollageView()}
|
||||
</ViewShot>
|
||||
<TouchableOpacity
|
||||
style={styles.previewPanel}
|
||||
activeOpacity={1}
|
||||
onPress={handleCanvasPress}
|
||||
>
|
||||
<ViewShot
|
||||
ref={viewShotRef}
|
||||
options={{ format: 'jpg', quality: 0.95 }}
|
||||
style={styles.viewShotContainer}
|
||||
>
|
||||
{renderCollageView()}
|
||||
|
||||
{/* Frame Overlay */}
|
||||
{activeFrame && <FrameOverlay frameAsset={activeFrame.asset} />}
|
||||
|
||||
{/* Date/Time Overlay */}
|
||||
{dateOverlay !== 'off' && (
|
||||
<View style={[styles.dateOverlay, getDatePositionStyle()]} pointerEvents="none">
|
||||
<Text style={styles.dateOverlayText}>
|
||||
{formatDate(dateOverlay)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Stickers */}
|
||||
{stickers.map((sticker) => (
|
||||
<DraggableSticker
|
||||
key={sticker.id}
|
||||
sticker={sticker}
|
||||
isSelected={selectedStickerId === sticker.id}
|
||||
onSelect={handleStickerSelect}
|
||||
onDelete={handleStickerDelete}
|
||||
onUpdate={handleStickerUpdate}
|
||||
/>
|
||||
))}
|
||||
</ViewShot>
|
||||
|
||||
{/* Sticker Tray (outside ViewShot — not captured) */}
|
||||
{showStickerTray && (
|
||||
<StickerTray
|
||||
onAddEmoji={handleAddEmoji}
|
||||
onAddDateSticker={handleAddDateSticker}
|
||||
onAddEventSticker={handleAddEventSticker}
|
||||
onClose={() => setShowStickerTray(false)}
|
||||
eventText={eventText}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Right panel: Controls */}
|
||||
<View style={styles.controlPanel}>
|
||||
@@ -252,28 +405,79 @@ export default function PreviewScreen({
|
||||
Aufgenommene Fotos: {photoUris.length} / 4
|
||||
</Text>
|
||||
|
||||
{/* Editing Tools Row */}
|
||||
<View style={styles.editToolsRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.editToolBtn, showStickerTray && styles.editToolBtnActive]}
|
||||
onPress={() => setShowStickerTray(!showStickerTray)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFaceGrinStars}
|
||||
size={16}
|
||||
color={showStickerTray ? THEME.colors.text : THEME.colors.accent}
|
||||
/>
|
||||
<Text style={[styles.editToolText, showStickerTray && styles.editToolTextActive]}>
|
||||
Sticker
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{frameMode === 'available' && (
|
||||
<TouchableOpacity
|
||||
style={[styles.editToolBtn, selectedFrameId !== null && styles.editToolBtnActive]}
|
||||
onPress={() => {
|
||||
if (selectedFrameId !== null) {
|
||||
setSelectedFrameId(null);
|
||||
}
|
||||
// FramePicker is shown below
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faBorderAll}
|
||||
size={16}
|
||||
color={selectedFrameId !== null ? THEME.colors.text : THEME.colors.accent}
|
||||
/>
|
||||
<Text style={[styles.editToolText, selectedFrameId !== null && styles.editToolTextActive]}>
|
||||
Rahmen
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Frame Picker (when mode = available) */}
|
||||
{frameMode === 'available' && (
|
||||
<FramePicker
|
||||
selectedFrameId={selectedFrameId}
|
||||
onSelectFrame={setSelectedFrameId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity style={[styles.actionBtn, styles.printBtn]} onPress={handlePrint}>
|
||||
<Text style={styles.actionBtnText}>JETZT DRUCKEN 🖨️</Text>
|
||||
<FontAwesomeIcon icon={faPrint} size={18} color={THEME.colors.text} style={{ marginRight: 8 }} />
|
||||
<Text style={styles.actionBtnText}>JETZT DRUCKEN</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.actionBtn, styles.saveBtn]} onPress={handleSaveOnly}>
|
||||
<Text style={styles.actionBtnText}>Nur auf Tablet speichern 💾</Text>
|
||||
<FontAwesomeIcon icon={faDownload} size={16} color={THEME.colors.accent} style={{ marginRight: 8 }} />
|
||||
<Text style={[styles.actionBtnText, { color: THEME.colors.accent }]}>Nur auf Tablet speichern</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{photoUris.length < 4 && (
|
||||
<TouchableOpacity style={[styles.actionBtn, styles.addBtn]} onPress={onAddAnother}>
|
||||
<Text style={styles.actionBtnText}>+ Weiteres Foto aufnehmen</Text>
|
||||
<FontAwesomeIcon icon={faPlus} size={16} color={THEME.colors.text} style={{ marginRight: 8 }} />
|
||||
<Text style={styles.actionBtnText}>Weiteres Foto aufnehmen</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={styles.rowActions}>
|
||||
<TouchableOpacity style={[styles.smallBtn, styles.retakeBtn]} onPress={onRetakeLast}>
|
||||
<Text style={styles.smallBtnText}>Letztes wiederholen</Text>
|
||||
<FontAwesomeIcon icon={faRotateLeft} size={14} color={THEME.colors.error} style={{ marginRight: 6 }} />
|
||||
<Text style={[styles.smallBtnText, { color: THEME.colors.error }]}>Wiederholen</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.smallBtn, styles.resetBtn]} onPress={handleExit}>
|
||||
<FontAwesomeIcon icon={faRightFromBracket} size={14} color={THEME.colors.textMuted} style={{ marginRight: 6 }} />
|
||||
<Text style={styles.smallBtnText}>Beenden</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -304,7 +508,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: THEME.spacing.md,
|
||||
backgroundColor: '#050507',
|
||||
backgroundColor: '#030308',
|
||||
},
|
||||
viewShotContainer: {
|
||||
width: 320,
|
||||
@@ -327,6 +531,9 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
marginBottom: THEME.spacing.md,
|
||||
letterSpacing: 2,
|
||||
textShadowColor: THEME.colors.neonGlow,
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
layoutSelector: {
|
||||
flexDirection: 'row',
|
||||
@@ -346,6 +553,11 @@ const styles = StyleSheet.create({
|
||||
layoutBtnActive: {
|
||||
backgroundColor: THEME.colors.primary,
|
||||
borderColor: THEME.colors.primary,
|
||||
shadowColor: THEME.colors.primary,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
layoutBtnDisabled: {
|
||||
opacity: 0.3,
|
||||
@@ -359,13 +571,43 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
color: THEME.colors.textMuted,
|
||||
textAlign: 'center',
|
||||
marginBottom: THEME.spacing.lg,
|
||||
marginBottom: THEME.spacing.sm,
|
||||
},
|
||||
editToolsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
marginBottom: THEME.spacing.md,
|
||||
},
|
||||
editToolBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: THEME.borderRadius.round,
|
||||
borderWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
backgroundColor: THEME.colors.surfaceSecondary,
|
||||
},
|
||||
editToolBtnActive: {
|
||||
backgroundColor: THEME.colors.primary,
|
||||
borderColor: THEME.colors.primary,
|
||||
},
|
||||
editToolText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: THEME.colors.accent,
|
||||
},
|
||||
editToolTextActive: {
|
||||
color: THEME.colors.text,
|
||||
},
|
||||
actions: {
|
||||
width: '100%',
|
||||
},
|
||||
actionBtn: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
paddingVertical: THEME.spacing.md,
|
||||
borderRadius: THEME.borderRadius.md,
|
||||
alignItems: 'center',
|
||||
@@ -374,6 +616,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
printBtn: {
|
||||
backgroundColor: THEME.colors.primary,
|
||||
shadowColor: THEME.colors.primary,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
},
|
||||
saveBtn: {
|
||||
backgroundColor: 'transparent',
|
||||
@@ -381,7 +628,7 @@ const styles = StyleSheet.create({
|
||||
borderColor: THEME.colors.border,
|
||||
},
|
||||
addBtn: {
|
||||
backgroundColor: THEME.colors.accent,
|
||||
backgroundColor: THEME.colors.accentDark,
|
||||
},
|
||||
actionBtnText: {
|
||||
color: THEME.colors.text,
|
||||
@@ -396,9 +643,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
smallBtn: {
|
||||
flex: 0.48,
|
||||
flexDirection: 'row',
|
||||
paddingVertical: THEME.spacing.sm,
|
||||
borderRadius: THEME.borderRadius.sm,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
},
|
||||
retakeBtn: {
|
||||
@@ -410,19 +659,20 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
smallBtnText: {
|
||||
color: THEME.colors.text,
|
||||
color: THEME.colors.textMuted,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
// ── Photo Canvases ──
|
||||
singleCanvas: {
|
||||
width: 320,
|
||||
height: 480,
|
||||
backgroundColor: '#fff',
|
||||
padding: 10,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowColor: THEME.colors.primary,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 10,
|
||||
shadowRadius: 15,
|
||||
elevation: 5,
|
||||
},
|
||||
singleImage: {
|
||||
@@ -496,6 +746,24 @@ const styles = StyleSheet.create({
|
||||
resizeMode: 'cover',
|
||||
borderRadius: 2,
|
||||
},
|
||||
// ── Date Overlay ──
|
||||
dateOverlay: {
|
||||
position: 'absolute',
|
||||
zIndex: 60,
|
||||
},
|
||||
dateOverlayText: {
|
||||
fontSize: 11,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textShadowOffset: { width: 1, height: 1 },
|
||||
textShadowRadius: 3,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.35)',
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
// ── Loading ──
|
||||
loadingOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
@@ -514,6 +782,11 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: THEME.colors.border,
|
||||
shadowColor: THEME.colors.primary,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 20,
|
||||
elevation: 10,
|
||||
},
|
||||
loadingText: {
|
||||
color: THEME.colors.text,
|
||||
|
||||
Reference in New Issue
Block a user