diff --git a/VinEye/src/components/my-plants/EditNameBottomSheet.tsx b/VinEye/src/components/my-plants/EditNameBottomSheet.tsx
new file mode 100644
index 0000000..856987e
--- /dev/null
+++ b/VinEye/src/components/my-plants/EditNameBottomSheet.tsx
@@ -0,0 +1,140 @@
+import { useEffect, useState } from "react";
+import {
+ View,
+ Pressable,
+ Modal,
+ KeyboardAvoidingView,
+ Platform,
+ TextInput,
+ TouchableWithoutFeedback,
+ Keyboard,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { useTranslation } from "react-i18next";
+import { X, Check } from "lucide-react-native";
+
+import { Text } from "@/components/ui/text";
+import { colors } from "@/theme/colors";
+
+interface EditNameBottomSheetProps {
+ initialName: string;
+ onSave: (newName: string) => void;
+ onClose: () => void;
+}
+
+export function EditNameBottomSheet({
+ initialName,
+ onSave,
+ onClose,
+}: EditNameBottomSheetProps) {
+ const { t } = useTranslation();
+ const insets = useSafeAreaInsets();
+ const [name, setName] = useState(initialName);
+
+ useEffect(() => {
+ setName(initialName);
+ }, [initialName]);
+
+ const trimmedName = name.trim();
+ const isDirty =
+ trimmedName.length > 0 && trimmedName !== initialName.trim();
+
+ function handleSave() {
+ if (!isDirty) return;
+ onSave(trimmedName);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("myPlants.detail.renameTitle")}
+
+
+
+
+
+
+
+ {t("myPlants.detail.renameSubtitle")}
+
+
+
+
+
+
+
+
+
+ {t("myPlants.actions.cancel")}
+
+
+
+
+
+
+
+ {t("myPlants.detail.renameSave")}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/VinEye/src/hooks/useScanDetail.ts b/VinEye/src/hooks/useScanDetail.ts
index ee5809d..bad7bf6 100644
--- a/VinEye/src/hooks/useScanDetail.ts
+++ b/VinEye/src/hooks/useScanDetail.ts
@@ -38,5 +38,33 @@ export function useScanDetail(scanId: string) {
await storage.set(storage.KEYS.SCAN_HISTORY, updated);
}, [scanId]);
- return { scan, loading, error, toggleFavorite, deleteScan, refetch: load };
+ const renameScan = useCallback(
+ async (newName: string) => {
+ const trimmed = newName.trim();
+ const all = await storage.get(storage.KEYS.SCAN_HISTORY);
+ if (!all) return;
+ const updated = all.map((s) =>
+ s.id === scanId
+ ? { ...s, customName: trimmed.length > 0 ? trimmed : undefined }
+ : s,
+ );
+ await storage.set(storage.KEYS.SCAN_HISTORY, updated);
+ setScan((prev) =>
+ prev
+ ? { ...prev, customName: trimmed.length > 0 ? trimmed : undefined }
+ : prev,
+ );
+ },
+ [scanId],
+ );
+
+ return {
+ scan,
+ loading,
+ error,
+ toggleFavorite,
+ deleteScan,
+ renameScan,
+ refetch: load,
+ };
}
diff --git a/VinEye/src/screens/ScanDetailScreen.tsx b/VinEye/src/screens/ScanDetailScreen.tsx
index 38eeafa..eb89ecd 100644
--- a/VinEye/src/screens/ScanDetailScreen.tsx
+++ b/VinEye/src/screens/ScanDetailScreen.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import {
View,
ScrollView,
@@ -27,6 +27,7 @@ import {
Share2,
Trash2,
AlertCircle,
+ Pencil,
} from "lucide-react-native";
import Animated, {
useSharedValue,
@@ -37,6 +38,7 @@ import Animated, {
import { toast } from "sonner-native";
import { Text } from "@/components/ui/text";
+import { EditNameBottomSheet } from "@/components/my-plants/EditNameBottomSheet";
import { useScanDetail } from "@/hooks/useScanDetail";
import { getCepageById } from "@/utils/cepages";
import { hapticSuccess } from "@/services/haptics";
@@ -94,8 +96,9 @@ export default function ScanDetailScreen({ route }: Props) {
const { t, i18n } = useTranslation();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
- const { scan, loading, error, toggleFavorite, deleteScan } =
+ const { scan, loading, error, toggleFavorite, deleteScan, renameScan } =
useScanDetail(scanId);
+ const [editingName, setEditingName] = useState(false);
// Entry animation
const contentY = useSharedValue(30);
@@ -160,11 +163,27 @@ export default function ScanDetailScreen({ route }: Props) {
const isFav = scan.isFavorite === true;
const hasImage = !!detection.imageUri;
- const heroTitle = cepage
+ const fallbackTitle = cepage
? cepage.name.fr
: detection.result === "vine"
? t("myPlants.detail.results.vine")
: t("myPlants.detail.results.unidentified");
+ const heroTitle = scan.customName?.trim() || fallbackTitle;
+
+ function handleOpenRename() {
+ setEditingName(true);
+ }
+
+ function handleCloseRename() {
+ setEditingName(false);
+ }
+
+ async function handleRenameSave(newName: string) {
+ await renameScan(newName);
+ setEditingName(false);
+ hapticSuccess();
+ toast.success(t("myPlants.toasts.renamed"));
+ }
async function handleToggleFavorite() {
await toggleFavorite();
@@ -281,6 +300,13 @@ export default function ScanDetailScreen({ route }: Props) {
fill={isFav ? "#FFB800" : "none"}
/>
+
+
+
{/* ── Content ── */}
@@ -440,6 +466,14 @@ export default function ScanDetailScreen({ route }: Props) {
+
+ {editingName && (
+
+ )}
);
}