1053 lines
38 KiB
TypeScript
1053 lines
38 KiB
TypeScript
import React, { useRef, useState } from "react";
|
||
import {
|
||
Pressable,
|
||
ScrollView,
|
||
Text,
|
||
TextInput,
|
||
View,
|
||
} from "react-native";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import * as Haptics from "expo-haptics";
|
||
import ViewShot from "react-native-view-shot";
|
||
import { CanvasLayerItem } from "./CanvasLayerItem";
|
||
import { CanvasQrItem } from "./CanvasQrItem";
|
||
import { InstagramTextEditorModal } from "./InstagramTextEditorModal";
|
||
import {
|
||
type CanvasTextLayer,
|
||
type CanvasQrConfig,
|
||
CANVAS_FONT_OPTIONS,
|
||
QR_SIZE_PRESETS,
|
||
QR_RADIUS_PRESETS,
|
||
getDefaultLayersForTemplate,
|
||
getDefaultQrConfigForTemplate,
|
||
} from "./types";
|
||
|
||
interface QrCanvasStudioProps {
|
||
templateKey: string;
|
||
restaurantName: string;
|
||
qrBase64?: string;
|
||
width: number;
|
||
height: number;
|
||
renderBackground: () => React.ReactNode;
|
||
layers: CanvasTextLayer[];
|
||
onLayersChange: React.Dispatch<React.SetStateAction<CanvasTextLayer[]>>;
|
||
qrConfig: CanvasQrConfig;
|
||
onQrConfigChange: React.Dispatch<React.SetStateAction<CanvasQrConfig>>;
|
||
viewShotRef?: React.RefObject<any>;
|
||
selectedLayerId: string | null;
|
||
onSelectLayer: (id: string | null) => void;
|
||
isQrSelected: boolean;
|
||
onSelectQr: (selected: boolean) => void;
|
||
onDragStateChange?: (isDragging: boolean) => void;
|
||
}
|
||
|
||
export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
|
||
templateKey,
|
||
restaurantName,
|
||
qrBase64,
|
||
width,
|
||
height,
|
||
renderBackground,
|
||
layers,
|
||
onLayersChange,
|
||
qrConfig,
|
||
onQrConfigChange,
|
||
viewShotRef,
|
||
selectedLayerId,
|
||
onSelectLayer,
|
||
isQrSelected,
|
||
onSelectQr,
|
||
onDragStateChange,
|
||
}) => {
|
||
const selectedLayer = layers.find((l) => l.id === selectedLayerId) || null;
|
||
|
||
// Instagram Metin Düzenleyici Modal Durumu
|
||
const [editingLayer, setEditingLayer] = useState<CanvasTextLayer | null>(null);
|
||
const [isTextModalOpen, setIsTextModalOpen] = useState(false);
|
||
|
||
// Instagram/Figma Kılavuz Çizgileri
|
||
const [guideLines, setGuideLines] = useState<{
|
||
verticalCenter: boolean;
|
||
horizontalCenter: boolean;
|
||
}>({ verticalCenter: false, horizontalCenter: false });
|
||
|
||
const lastVRef = useRef(false);
|
||
const lastHRef = useRef(false);
|
||
|
||
// Sürükleme Başlangıcı
|
||
const handleDragStart = () => {
|
||
onDragStateChange?.(true);
|
||
};
|
||
|
||
// Manyetik Yapışma (Snap) & Kılavuz Çizgileri
|
||
const handleSnap = (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => {
|
||
const SNAP_THRESHOLD = 6;
|
||
let snappedX = Math.round(rawX);
|
||
let snappedY = Math.round(rawY);
|
||
let vCenter = false;
|
||
let hCenter = false;
|
||
|
||
// 1. Dikey Kılavuz (Yatayda Tam Ortalandığında)
|
||
const itemCenterX = rawX + itemWidth / 2;
|
||
const canvasCenterX = width / 2;
|
||
if (Math.abs(itemCenterX - canvasCenterX) <= SNAP_THRESHOLD) {
|
||
snappedX = Math.round(canvasCenterX - itemWidth / 2);
|
||
vCenter = true;
|
||
}
|
||
|
||
// 2. Yatay Kılavuz (Dikeyde Tam Ortalandığında)
|
||
const itemCenterY = rawY + itemHeight / 2;
|
||
const canvasCenterY = height / 2;
|
||
if (Math.abs(itemCenterY - canvasCenterY) <= SNAP_THRESHOLD) {
|
||
snappedY = Math.round(canvasCenterY - itemHeight / 2);
|
||
hCenter = true;
|
||
}
|
||
|
||
// Dokunsal Titreşim
|
||
if ((vCenter && !lastVRef.current) || (hCenter && !lastHRef.current)) {
|
||
try {
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||
} catch {}
|
||
}
|
||
lastVRef.current = vCenter;
|
||
lastHRef.current = hCenter;
|
||
|
||
setGuideLines({ verticalCenter: vCenter, horizontalCenter: hCenter });
|
||
return { snappedX, snappedY };
|
||
};
|
||
|
||
// Sürükleme Bitişi
|
||
const handleDragEnd = () => {
|
||
setGuideLines({ verticalCenter: false, horizontalCenter: false });
|
||
lastVRef.current = false;
|
||
lastHRef.current = false;
|
||
onDragStateChange?.(false);
|
||
};
|
||
|
||
// Metin Katmanı Pozisyon Güncelleme
|
||
const handleUpdateLayerPosition = (id: string, x: number, y: number) => {
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === id ? { ...l, x, y } : l))
|
||
);
|
||
};
|
||
|
||
// QR Kod Pozisyon Güncelleme
|
||
const handleUpdateQrPosition = (x: number, y: number) => {
|
||
onQrConfigChange((prev) => ({ ...prev, x, y }));
|
||
};
|
||
|
||
// QR Kodu Yatayda Ortala
|
||
const handleCenterQrHorizontally = () => {
|
||
const centeredX = Math.round((width - qrConfig.size) / 2);
|
||
onQrConfigChange((prev) => ({ ...prev, x: centeredX }));
|
||
try {
|
||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||
} catch {}
|
||
};
|
||
|
||
// Seçili Metni Yatayda Ortala
|
||
const handleCenterLayerHorizontally = (id: string) => {
|
||
const target = layers.find((l) => l.id === id);
|
||
if (!target) return;
|
||
const approxW = 80;
|
||
const centeredX = Math.round((width - approxW) / 2);
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === id ? { ...l, x: centeredX } : l))
|
||
);
|
||
try {
|
||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||
} catch {}
|
||
};
|
||
|
||
// Yeni Metin Katmanı Ekle (Instagram "Aa" butonu)
|
||
const handleAddTextLayer = () => {
|
||
try {
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||
} catch {}
|
||
const newLayer: CanvasTextLayer = {
|
||
id: `layer_${Date.now()}`,
|
||
text: "Yeni Metin",
|
||
x: Math.round(width / 2 - 45),
|
||
y: Math.round(height / 2 - 20),
|
||
rotation: 0,
|
||
fontSize: 16,
|
||
fontId: "sans",
|
||
color: "#FFFFFF",
|
||
bgBoxColor: "rgba(18, 18, 21, 0.88)",
|
||
fontWeight: "800",
|
||
textAlign: "center",
|
||
};
|
||
onLayersChange((prev) => [...prev, newLayer]);
|
||
onSelectQr(false);
|
||
onSelectLayer(newLayer.id);
|
||
|
||
// Doğrudan Instagram Text Editor'ı aç
|
||
setEditingLayer(newLayer);
|
||
setIsTextModalOpen(true);
|
||
};
|
||
|
||
// Katman Düzenle Modalını Aç
|
||
const handleOpenTextEditor = (layer: CanvasTextLayer) => {
|
||
setEditingLayer(layer);
|
||
setIsTextModalOpen(true);
|
||
};
|
||
|
||
// Katman Düzenleme Kaydet
|
||
const handleSaveTextEditor = (updated: Partial<CanvasTextLayer>) => {
|
||
if (!editingLayer) return;
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === editingLayer.id ? { ...l, ...updated } : l))
|
||
);
|
||
};
|
||
|
||
// Katman Sil
|
||
const handleDeleteLayer = (id: string) => {
|
||
try {
|
||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||
} catch {}
|
||
onLayersChange((prev) => prev.filter((l) => l.id !== id));
|
||
if (selectedLayerId === id) {
|
||
onSelectLayer(null);
|
||
}
|
||
};
|
||
|
||
// Katman Çoğalt
|
||
const handleDuplicateLayer = (id: string) => {
|
||
try {
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||
} catch {}
|
||
const target = layers.find((l) => l.id === id);
|
||
if (!target) return;
|
||
const duplicated: CanvasTextLayer = {
|
||
...target,
|
||
id: `layer_${Date.now()}`,
|
||
x: Math.min(width - 50, target.x + 12),
|
||
y: Math.min(height - 30, target.y + 12),
|
||
};
|
||
onLayersChange((prev) => [...prev, duplicated]);
|
||
onSelectQr(false);
|
||
onSelectLayer(duplicated.id);
|
||
};
|
||
|
||
// Şablonu Sıfırla
|
||
const handleResetLayers = () => {
|
||
try {
|
||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||
} catch {}
|
||
const defaultLayers = getDefaultLayersForTemplate(templateKey, restaurantName);
|
||
const defaultQr = getDefaultQrConfigForTemplate(templateKey, width, height);
|
||
onLayersChange(defaultLayers);
|
||
onQrConfigChange(defaultQr);
|
||
onSelectQr(false);
|
||
onSelectLayer(null);
|
||
};
|
||
|
||
return (
|
||
<View style={{ width: "100%", alignItems: "center" }}>
|
||
{/* 1. INSTAGRAM STORY / CANVA STİLİ ÜST HIZLI ARAÇLAR */}
|
||
<View
|
||
style={{
|
||
width: "100%",
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
marginBottom: 12,
|
||
paddingHorizontal: 4,
|
||
}}
|
||
>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||
{/* Metin Ekle (+ Aa) */}
|
||
<Pressable
|
||
onPress={handleAddTextLayer}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
accessible={true}
|
||
accessibilityRole="button"
|
||
accessibilityLabel="Yeni Metin Ekle"
|
||
style={({ pressed }) => ({
|
||
backgroundColor: "#D4AF37",
|
||
paddingHorizontal: 14,
|
||
paddingVertical: 8,
|
||
borderRadius: 20,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
opacity: pressed ? 0.8 : 1,
|
||
})}
|
||
>
|
||
<Text style={{ color: "#0C0C0E", fontSize: 13, fontWeight: "900" }}>+ Aa</Text>
|
||
<Text style={{ color: "#0C0C0E", fontSize: 12, fontWeight: "700" }}>Metin</Text>
|
||
</Pressable>
|
||
|
||
{/* QR Seçici / Ayar */}
|
||
<Pressable
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onSelectLayer(null);
|
||
onSelectQr(!isQrSelected);
|
||
}}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={({ pressed }) => ({
|
||
backgroundColor: isQrSelected ? "#D4AF37" : "#18181B",
|
||
borderWidth: 1,
|
||
borderColor: isQrSelected ? "#D4AF37" : "rgba(255,255,255,0.12)",
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 8,
|
||
borderRadius: 20,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
opacity: pressed ? 0.8 : 1,
|
||
})}
|
||
>
|
||
<Ionicons
|
||
name="qr-code-outline"
|
||
size={15}
|
||
color={isQrSelected ? "#0C0C0E" : "#FFFFFF"}
|
||
/>
|
||
<Text
|
||
style={{
|
||
color: isQrSelected ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 12,
|
||
fontWeight: "700",
|
||
}}
|
||
>
|
||
QR Kodu
|
||
</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* Sağ: Sıfırla */}
|
||
<Pressable
|
||
onPress={handleResetLayers}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={({ pressed }) => ({
|
||
backgroundColor: "#18181B",
|
||
borderWidth: 1,
|
||
borderColor: "rgba(255,255,255,0.1)",
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 7,
|
||
borderRadius: 16,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
opacity: pressed ? 0.8 : 1,
|
||
})}
|
||
>
|
||
<Ionicons name="refresh-outline" size={14} color="#A1A1AA" />
|
||
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "600" }}>Sıfırla</Text>
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* 2. TUVAL ALANI (INSTAGRAM KORUYUCU DOKUNMATİK ÇERÇEVE) */}
|
||
<Pressable
|
||
onPress={() => {
|
||
// Tuvalin boş alanına dokunulduğunda seçimi kaldır (temiz önizleme)
|
||
onSelectLayer(null);
|
||
onSelectQr(false);
|
||
}}
|
||
style={{
|
||
width,
|
||
height,
|
||
borderRadius: 24,
|
||
overflow: "hidden",
|
||
backgroundColor: "#0C0C0E",
|
||
shadowColor: "#000",
|
||
shadowOffset: { width: 0, height: 10 },
|
||
shadowOpacity: 0.5,
|
||
shadowRadius: 20,
|
||
elevation: 8,
|
||
marginBottom: 14,
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<ViewShot
|
||
ref={viewShotRef}
|
||
options={{ format: "png", quality: 1.0 }}
|
||
style={{ width, height, position: "relative" }}
|
||
>
|
||
{/* Arka Plan Şablonu */}
|
||
<View pointerEvents="none" style={{ position: "absolute", top: 0, left: 0, width, height }}>
|
||
{renderBackground()}
|
||
</View>
|
||
|
||
{/* Sürüklenebilir & Boyutlandırılabilir QR Kod */}
|
||
<CanvasQrItem
|
||
qrConfig={qrConfig}
|
||
qrBase64={qrBase64}
|
||
isSelected={isQrSelected}
|
||
onSelect={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onSelectLayer(null);
|
||
onSelectQr(true);
|
||
}}
|
||
onUpdatePosition={handleUpdateQrPosition}
|
||
onDragStart={handleDragStart}
|
||
onDragMove={handleSnap}
|
||
onDragEnd={handleDragEnd}
|
||
canvasWidth={width}
|
||
canvasHeight={height}
|
||
/>
|
||
|
||
{/* Sürüklenebilir & Çift Dokunmalı Metin Katmanları */}
|
||
{layers.map((layer) => (
|
||
<CanvasLayerItem
|
||
key={layer.id}
|
||
layer={layer}
|
||
isSelected={selectedLayerId === layer.id}
|
||
onSelect={(id) => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onSelectQr(false);
|
||
onSelectLayer(id);
|
||
}}
|
||
onEdit={handleOpenTextEditor}
|
||
onUpdatePosition={handleUpdateLayerPosition}
|
||
onDelete={handleDeleteLayer}
|
||
onDragStart={handleDragStart}
|
||
onDragMove={handleSnap}
|
||
onDragEnd={handleDragEnd}
|
||
canvasWidth={width}
|
||
canvasHeight={height}
|
||
/>
|
||
))}
|
||
</ViewShot>
|
||
|
||
{/* 📐 FIGMA / INSTAGRAM MANYETİK KILAVUZ ÇİZGİLERİ */}
|
||
{guideLines.verticalCenter && (
|
||
<View
|
||
pointerEvents="none"
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
bottom: 0,
|
||
left: Math.round(width / 2) - 0.75,
|
||
width: 1.5,
|
||
backgroundColor: "#D4AF37",
|
||
zIndex: 999,
|
||
shadowColor: "#D4AF37",
|
||
shadowOffset: { width: 0, height: 0 },
|
||
shadowOpacity: 0.9,
|
||
shadowRadius: 4,
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{guideLines.horizontalCenter && (
|
||
<View
|
||
pointerEvents="none"
|
||
style={{
|
||
position: "absolute",
|
||
left: 0,
|
||
right: 0,
|
||
top: Math.round(height / 2) - 0.75,
|
||
height: 1.5,
|
||
backgroundColor: "#D4AF37",
|
||
zIndex: 999,
|
||
shadowColor: "#D4AF37",
|
||
shadowOffset: { width: 0, height: 0 },
|
||
shadowOpacity: 0.9,
|
||
shadowRadius: 4,
|
||
}}
|
||
/>
|
||
)}
|
||
</Pressable>
|
||
|
||
{/* 3. SEÇİLİ ÖĞE İÇİN INSTAGRAM TARZI YÜZEN HIZLI ARAÇLAR (FLOATING DOCK) */}
|
||
{/* 3A: SEÇİLİ METİN KATMANI ARAÇLARI */}
|
||
{selectedLayer && !isQrSelected && (
|
||
<View
|
||
style={{
|
||
width: "100%",
|
||
backgroundColor: "#18181B",
|
||
borderRadius: 18,
|
||
borderWidth: 1,
|
||
borderColor: "rgba(212, 175, 55, 0.3)",
|
||
padding: 12,
|
||
marginBottom: 14,
|
||
gap: 10,
|
||
}}
|
||
>
|
||
{/* Üst Sıra: Hızlı Butonlar */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "800", letterSpacing: 0.5 }}>
|
||
SEÇİLİ METİN
|
||
</Text>
|
||
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||
{/* Düzenle (Aa) Modalını Aç */}
|
||
<Pressable
|
||
onPress={() => handleOpenTextEditor(selectedLayer)}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{
|
||
backgroundColor: "#D4AF37",
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 5,
|
||
borderRadius: 14,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
}}
|
||
>
|
||
<Ionicons name="create-outline" size={13} color="#0C0C0E" />
|
||
<Text style={{ color: "#0C0C0E", fontSize: 11, fontWeight: "800" }}>Aa Düzenle</Text>
|
||
</Pressable>
|
||
|
||
{/* Yatayda Ortala */}
|
||
<Pressable
|
||
onPress={() => handleCenterLayerHorizontally(selectedLayer.id)}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{
|
||
backgroundColor: "#242429",
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 5,
|
||
borderRadius: 14,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
}}
|
||
>
|
||
<Ionicons name="swap-horizontal" size={12} color="#FFFFFF" />
|
||
<Text style={{ color: "#FFFFFF", fontSize: 11, fontWeight: "600" }}>Ortala</Text>
|
||
</Pressable>
|
||
|
||
{/* Çoğalt */}
|
||
<Pressable
|
||
onPress={() => handleDuplicateLayer(selectedLayer.id)}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{
|
||
backgroundColor: "#242429",
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 5,
|
||
borderRadius: 14,
|
||
}}
|
||
>
|
||
<Ionicons name="copy-outline" size={13} color="#FFFFFF" />
|
||
</Pressable>
|
||
|
||
{/* Sil */}
|
||
<Pressable
|
||
onPress={() => handleDeleteLayer(selectedLayer.id)}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{
|
||
backgroundColor: "rgba(239, 68, 68, 0.15)",
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 5,
|
||
borderRadius: 14,
|
||
}}
|
||
>
|
||
<Ionicons name="trash-outline" size={13} color="#EF4444" />
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Hızlı Metin Değiştirme Girdisi */}
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
backgroundColor: "#0C0C0E",
|
||
borderRadius: 12,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
borderWidth: 1,
|
||
borderColor: "rgba(212, 175, 55, 0.4)",
|
||
}}
|
||
>
|
||
<Ionicons name="text-outline" size={14} color="#D4AF37" />
|
||
<TextInput
|
||
value={selectedLayer.text}
|
||
onChangeText={(newText: string) => {
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === selectedLayer.id ? { ...l, text: newText } : l))
|
||
);
|
||
}}
|
||
placeholder="Metin yazın..."
|
||
placeholderTextColor="#71717A"
|
||
style={{
|
||
flex: 1,
|
||
color: "#FFFFFF",
|
||
fontSize: 13,
|
||
fontWeight: "700",
|
||
padding: 0,
|
||
}}
|
||
/>
|
||
</View>
|
||
|
||
{/* Alt Sıra 1: Font Stili Seçici */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>FONT:</Text>
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||
{CANVAS_FONT_OPTIONS.map((f) => {
|
||
const isActive = (selectedLayer.fontId || "sans") === f.id;
|
||
return (
|
||
<Pressable
|
||
key={f.id}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === selectedLayer.id ? { ...l, fontId: f.id } : l))
|
||
);
|
||
}}
|
||
style={{
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
borderRadius: 10,
|
||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 10.5,
|
||
fontWeight: isActive ? "900" : "700",
|
||
fontFamily: f.fontFamily,
|
||
}}
|
||
>
|
||
{f.label}
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
</View>
|
||
|
||
{/* Alt Sıra 2: Hızlı Döndürme Açısı Hapları */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>EĞİM:</Text>
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||
{[-15, -6, 0, 6, 15].map((deg) => {
|
||
const isActive = (selectedLayer.rotation || 0) === deg;
|
||
return (
|
||
<Pressable
|
||
key={deg}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === selectedLayer.id ? { ...l, rotation: deg } : l))
|
||
);
|
||
}}
|
||
style={{
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 4,
|
||
borderRadius: 10,
|
||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 10.5,
|
||
fontWeight: isActive ? "800" : "600",
|
||
}}
|
||
>
|
||
{deg}°
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
</View>
|
||
|
||
{/* Alt Sıra 3: Kavis (Eğri Yazı) Stepper & Preset Hapları */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KAVİS:</Text>
|
||
|
||
{/* Stepper Butonları (- / +) */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", backgroundColor: "#242429", borderRadius: 10, paddingHorizontal: 4, paddingVertical: 2, gap: 4 }}>
|
||
<Pressable
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onLayersChange((prev) =>
|
||
prev.map((l) =>
|
||
l.id === selectedLayer.id
|
||
? { ...l, curveDegree: Math.max(-100, (l.curveDegree || 0) - 5) }
|
||
: l
|
||
)
|
||
);
|
||
}}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
|
||
>
|
||
<Ionicons name="remove" size={13} color="#FFFFFF" />
|
||
</Pressable>
|
||
|
||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "900", minWidth: 30, textAlign: "center" }}>
|
||
{(selectedLayer.curveDegree || 0)}°
|
||
</Text>
|
||
|
||
<Pressable
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onLayersChange((prev) =>
|
||
prev.map((l) =>
|
||
l.id === selectedLayer.id
|
||
? { ...l, curveDegree: Math.min(100, (l.curveDegree || 0) + 5) }
|
||
: l
|
||
)
|
||
);
|
||
}}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
|
||
>
|
||
<Ionicons name="add" size={13} color="#FFFFFF" />
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* Hazır Kavis Presetleri */}
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||
{[
|
||
{ label: "0° (Düz)", deg: 0 },
|
||
{ label: "20° ◠", deg: 20 },
|
||
{ label: "35° ◠", deg: 35 },
|
||
{ label: "50° ◠", deg: 50 },
|
||
{ label: "70° ◠", deg: 70 },
|
||
{ label: "-35° ◡", deg: -35 },
|
||
{ label: "-50° ◡", deg: -50 },
|
||
].map((item) => {
|
||
const isActive = (selectedLayer.curveDegree || 0) === item.deg;
|
||
return (
|
||
<Pressable
|
||
key={item.deg}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onLayersChange((prev) =>
|
||
prev.map((l) => (l.id === selectedLayer.id ? { ...l, curveDegree: item.deg } : l))
|
||
);
|
||
}}
|
||
style={{
|
||
paddingHorizontal: 9,
|
||
paddingVertical: 4,
|
||
borderRadius: 10,
|
||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 10.5,
|
||
fontWeight: isActive ? "800" : "600",
|
||
}}
|
||
>
|
||
{item.label}
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 3B: SEÇİLİ QR KOD ARAÇLARI (TAM GELİŞMİŞ AYARLAR) */}
|
||
{isQrSelected && (
|
||
<View
|
||
style={{
|
||
width: "100%",
|
||
backgroundColor: "#18181B",
|
||
borderRadius: 20,
|
||
borderWidth: 1.5,
|
||
borderColor: "rgba(212, 175, 55, 0.45)",
|
||
padding: 14,
|
||
marginBottom: 14,
|
||
gap: 12,
|
||
shadowColor: "#000",
|
||
shadowOffset: { width: 0, height: 6 },
|
||
shadowOpacity: 0.35,
|
||
shadowRadius: 10,
|
||
elevation: 6,
|
||
}}
|
||
>
|
||
{/* Üst Başlık & Ortala / Kapat */}
|
||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||
<Ionicons name="qr-code" size={15} color="#D4AF37" />
|
||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "800", letterSpacing: 0.5 }}>
|
||
QR KOD AYARLARI
|
||
</Text>
|
||
</View>
|
||
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||
<Pressable
|
||
onPress={handleCenterQrHorizontally}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={({ pressed }) => ({
|
||
backgroundColor: "#242429",
|
||
paddingHorizontal: 11,
|
||
paddingVertical: 5,
|
||
borderRadius: 14,
|
||
borderWidth: 1,
|
||
borderColor: "rgba(212, 175, 55, 0.3)",
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
opacity: pressed ? 0.8 : 1,
|
||
})}
|
||
>
|
||
<Ionicons name="swap-horizontal" size={13} color="#D4AF37" />
|
||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>Ortala</Text>
|
||
</Pressable>
|
||
|
||
<Pressable
|
||
onPress={() => onSelectQr(false)}
|
||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||
style={{
|
||
backgroundColor: "rgba(255,255,255,0.08)",
|
||
paddingHorizontal: 9,
|
||
paddingVertical: 5,
|
||
borderRadius: 14,
|
||
}}
|
||
>
|
||
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "600" }}>Kapat</Text>
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 1. Hassas Boyut Stepper'ı & Hızlı Seçenekler */}
|
||
<View style={{ gap: 6 }}>
|
||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10.5, fontWeight: "700" }}>BOYUT:</Text>
|
||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||
<Pressable
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onQrConfigChange((prev) => ({ ...prev, size: Math.max(70, prev.size - 5) }));
|
||
}}
|
||
style={{
|
||
backgroundColor: "#27272A",
|
||
width: 28,
|
||
height: 24,
|
||
borderRadius: 8,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
>
|
||
<Text style={{ color: "#FFF", fontWeight: "800", fontSize: 13 }}>-</Text>
|
||
</Pressable>
|
||
|
||
<View
|
||
style={{
|
||
backgroundColor: "#0C0C0E",
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 3,
|
||
borderRadius: 6,
|
||
borderWidth: 1,
|
||
borderColor: "rgba(255,255,255,0.1)",
|
||
}}
|
||
>
|
||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "800", fontVariant: ["tabular-nums"] }}>
|
||
{qrConfig.size} px
|
||
</Text>
|
||
</View>
|
||
|
||
<Pressable
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onQrConfigChange((prev) => ({ ...prev, size: Math.min(220, prev.size + 5) }));
|
||
}}
|
||
style={{
|
||
backgroundColor: "#27272A",
|
||
width: 28,
|
||
height: 24,
|
||
borderRadius: 8,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
}}
|
||
>
|
||
<Text style={{ color: "#FFF", fontWeight: "800", fontSize: 13 }}>+</Text>
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={{ flexDirection: "row", gap: 5 }}>
|
||
{[
|
||
{ label: "Kompakt", size: 95 },
|
||
{ label: "Standart", size: 115 },
|
||
{ label: "Büyük", size: 135 },
|
||
{ label: "Geniş", size: 155 },
|
||
].map((p) => {
|
||
const isActive = Math.abs(qrConfig.size - p.size) <= 5;
|
||
return (
|
||
<Pressable
|
||
key={p.label}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onQrConfigChange((prev) => ({ ...prev, size: p.size }));
|
||
}}
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||
paddingVertical: 5,
|
||
borderRadius: 8,
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 10,
|
||
fontWeight: isActive ? "800" : "600",
|
||
}}
|
||
>
|
||
{p.label}
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 2. Kenarlık Rengi (Border Color) */}
|
||
<View style={{ gap: 6 }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10.5, fontWeight: "700" }}>ÇERÇEVE KENARLIK RENGİ:</Text>
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, alignItems: "center" }}>
|
||
{[
|
||
{ color: "#D4AF37", name: "Altın" },
|
||
{ color: "#F6EFE2", name: "Krem" },
|
||
{ color: "#FFFFFF", name: "Beyaz" },
|
||
{ color: "#18181B", name: "Siyah" },
|
||
{ color: "#8C6239", name: "Bronz" },
|
||
{ color: "#0C4443", name: "Zümrüt" },
|
||
{ color: "#B92B27", name: "Kırmızı" },
|
||
{ color: "transparent", name: "Yok" },
|
||
].map((item) => {
|
||
const isSelected = (qrConfig.borderColor || "#D4AF37").toLowerCase() === item.color.toLowerCase();
|
||
return (
|
||
<Pressable
|
||
key={item.name}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onQrConfigChange((prev) => ({ ...prev, borderColor: item.color }));
|
||
}}
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 4,
|
||
borderRadius: 10,
|
||
backgroundColor: isSelected ? "rgba(212, 175, 55, 0.25)" : "#242429",
|
||
borderWidth: 1.5,
|
||
borderColor: isSelected ? "#D4AF37" : "transparent",
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
width: 14,
|
||
height: 14,
|
||
borderRadius: 7,
|
||
backgroundColor: item.color === "transparent" ? "#3F3F46" : item.color,
|
||
borderWidth: 1,
|
||
borderColor: "rgba(255,255,255,0.4)",
|
||
}}
|
||
/>
|
||
<Text style={{ color: isSelected ? "#D4AF37" : "#E4E4E7", fontSize: 10.5, fontWeight: "700" }}>
|
||
{item.name}
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
</View>
|
||
|
||
{/* 3. Köşe Yuvarlaklığı & Kenarlık Kalınlığı */}
|
||
<View style={{ flexDirection: "row", gap: 10 }}>
|
||
{/* Köşe */}
|
||
<View style={{ flex: 1, gap: 5 }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KÖŞE OVAL:</Text>
|
||
<View style={{ flexDirection: "row", gap: 4 }}>
|
||
{[
|
||
{ label: "Düz", radius: 0 },
|
||
{ label: "Hafif", radius: 8 },
|
||
{ label: "Zarif", radius: 14 },
|
||
{ label: "Yuvarlak", radius: 24 },
|
||
].map((r) => {
|
||
const isActive = qrConfig.borderRadius === r.radius;
|
||
return (
|
||
<Pressable
|
||
key={r.label}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onQrConfigChange((prev) => ({ ...prev, borderRadius: r.radius }));
|
||
}}
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||
paddingVertical: 5,
|
||
borderRadius: 7,
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 9.5,
|
||
fontWeight: isActive ? "800" : "600",
|
||
}}
|
||
>
|
||
{r.label}
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
|
||
{/* Kalınlık */}
|
||
<View style={{ flex: 1, gap: 5 }}>
|
||
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KALINLIK:</Text>
|
||
<View style={{ flexDirection: "row", gap: 4 }}>
|
||
{[
|
||
{ label: "0px", width: 0 },
|
||
{ label: "1px", width: 1 },
|
||
{ label: "2px", width: 2 },
|
||
{ label: "3px", width: 3 },
|
||
].map((w) => {
|
||
const isActive = (qrConfig.borderWidth ?? 2) === w.width;
|
||
return (
|
||
<Pressable
|
||
key={w.label}
|
||
onPress={() => {
|
||
try { Haptics.selectionAsync(); } catch {}
|
||
onQrConfigChange((prev) => ({ ...prev, borderWidth: w.width }));
|
||
}}
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||
paddingVertical: 5,
|
||
borderRadius: 7,
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||
fontSize: 9.5,
|
||
fontWeight: isActive ? "800" : "600",
|
||
}}
|
||
>
|
||
{w.label}
|
||
</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 4. INSTAGRAM METİN DÜZENLEME MODALI */}
|
||
<InstagramTextEditorModal
|
||
visible={isTextModalOpen}
|
||
layer={editingLayer}
|
||
onSave={handleSaveTextEditor}
|
||
onClose={() => {
|
||
setIsTextModalOpen(false);
|
||
setEditingLayer(null);
|
||
}}
|
||
onDelete={handleDeleteLayer}
|
||
/>
|
||
</View>
|
||
);
|
||
};
|