add image switch and image delete
This commit is contained in:
@@ -348,6 +348,22 @@ def dataset_list():
|
|||||||
def dataset_image(filename):
|
def dataset_image(filename):
|
||||||
return send_from_directory(DATASET_DIR, filename)
|
return send_from_directory(DATASET_DIR, filename)
|
||||||
|
|
||||||
|
@app.route("/dataset/images/<filename>", methods=['DELETE'])
|
||||||
|
def delete_dataset_image(filename):
|
||||||
|
"""Elimina una imagen del dataset"""
|
||||||
|
try:
|
||||||
|
filepath = os.path.join(DATASET_DIR, filename)
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
# Invalidar cache
|
||||||
|
dataset_cache['timestamp'] = 0
|
||||||
|
print(f"🗑️ Deleted from dataset: {filename}")
|
||||||
|
return {"success": True, "message": f"Deleted {filename}"}
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": "File not found"}, 404
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": str(e)}, 500
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
t = threading.Thread(target=camera_loop, daemon=True)
|
t = threading.Thread(target=camera_loop, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ function AdminDashboard({ token }) {
|
|||||||
const [showDatasetModal, setShowDatasetModal] = useState(false);
|
const [showDatasetModal, setShowDatasetModal] = useState(false);
|
||||||
const [datasetImages, setDatasetImages] = useState([]);
|
const [datasetImages, setDatasetImages] = useState([]);
|
||||||
const [selectedImage, setSelectedImage] = useState(null);
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
const [selectedImageIndex, setSelectedImageIndex] = useState(-1);
|
||||||
const [datasetPage, setDatasetPage] = useState(1);
|
const [datasetPage, setDatasetPage] = useState(1);
|
||||||
const [datasetTotalPages, setDatasetTotalPages] = useState(1);
|
const [datasetTotalPages, setDatasetTotalPages] = useState(1);
|
||||||
const [datasetTotal, setDatasetTotal] = useState(0);
|
const [datasetTotal, setDatasetTotal] = useState(0);
|
||||||
|
const [deletingImage, setDeletingImage] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -67,6 +69,35 @@ function AdminDashboard({ token }) {
|
|||||||
};
|
};
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
|
// Keyboard navigation for dataset gallery
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (!showDatasetModal || !selectedImage) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft' && selectedImageIndex > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedImageIndex(prev => {
|
||||||
|
const newIndex = prev - 1;
|
||||||
|
setSelectedImage(datasetImages[newIndex]);
|
||||||
|
return newIndex;
|
||||||
|
});
|
||||||
|
} else if (e.key === 'ArrowRight' && selectedImageIndex < datasetImages.length - 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedImageIndex(prev => {
|
||||||
|
const newIndex = prev + 1;
|
||||||
|
setSelectedImage(datasetImages[newIndex]);
|
||||||
|
return newIndex;
|
||||||
|
});
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setSelectedImage(null);
|
||||||
|
setSelectedImageIndex(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [showDatasetModal, selectedImage, selectedImageIndex, datasetImages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewMode === 'history' && activeTab === 'monitor') {
|
if (viewMode === 'history' && activeTab === 'monitor') {
|
||||||
fetchHistory(selectedDate);
|
fetchHistory(selectedDate);
|
||||||
@@ -138,6 +169,52 @@ function AdminDashboard({ token }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectImageByIndex = (index) => {
|
||||||
|
if (index >= 0 && index < datasetImages.length) {
|
||||||
|
setSelectedImage(datasetImages[index]);
|
||||||
|
setSelectedImageIndex(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateImage = (direction) => {
|
||||||
|
const newIndex = selectedImageIndex + direction;
|
||||||
|
if (newIndex >= 0 && newIndex < datasetImages.length) {
|
||||||
|
selectImageByIndex(newIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCurrentImage = async () => {
|
||||||
|
if (!selectedImage || deletingImage) return;
|
||||||
|
|
||||||
|
if (!confirm(`¿Eliminar imagen de ${selectedImage.plate}?`)) return;
|
||||||
|
|
||||||
|
setDeletingImage(true);
|
||||||
|
try {
|
||||||
|
await axios.delete(`/dataset/images/${selectedImage.filename}`);
|
||||||
|
|
||||||
|
// Remover de la lista local
|
||||||
|
const newImages = datasetImages.filter((_, idx) => idx !== selectedImageIndex);
|
||||||
|
setDatasetImages(newImages);
|
||||||
|
setDatasetTotal(prev => prev - 1);
|
||||||
|
setDatasetCount(prev => prev - 1);
|
||||||
|
|
||||||
|
// Navegar a la siguiente imagen o cerrar si no hay más
|
||||||
|
if (newImages.length === 0) {
|
||||||
|
setSelectedImage(null);
|
||||||
|
setSelectedImageIndex(-1);
|
||||||
|
} else if (selectedImageIndex >= newImages.length) {
|
||||||
|
selectImageByIndex(newImages.length - 1);
|
||||||
|
} else {
|
||||||
|
setSelectedImage(newImages[selectedImageIndex]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting image:', err);
|
||||||
|
alert('Error al eliminar la imagen');
|
||||||
|
} finally {
|
||||||
|
setDeletingImage(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearchRut = (e) => {
|
const handleSearchRut = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const normalizedRut = searchRut.replace(/\./g, '').toUpperCase();
|
const normalizedRut = searchRut.replace(/\./g, '').toUpperCase();
|
||||||
@@ -539,23 +616,67 @@ function AdminDashboard({ token }) {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-auto p-4">
|
<div className="flex-1 overflow-auto p-4">
|
||||||
{selectedImage ? (
|
{selectedImage ? (
|
||||||
/* Image Preview */
|
/* Image Preview with Navigation */
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
{/* Top Controls */}
|
||||||
|
<div className="w-full flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedImage(null)}
|
onClick={() => { setSelectedImage(null); setSelectedImageIndex(-1); }}
|
||||||
className="self-start flex items-center gap-2 text-slate-400 hover:text-white transition-colors"
|
className="flex items-center gap-2 text-slate-400 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
← Volver a galería
|
← Volver a galería
|
||||||
</button>
|
</button>
|
||||||
|
<div className="text-slate-400 text-sm">
|
||||||
|
{selectedImageIndex + 1} de {datasetImages.length}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={deleteCurrentImage}
|
||||||
|
disabled={deletingImage}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 bg-red-600 hover:bg-red-500 disabled:bg-red-800 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
{deletingImage ? 'Eliminando...' : 'Eliminar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image with Navigation Arrows */}
|
||||||
|
<div className="relative flex items-center gap-4 w-full justify-center">
|
||||||
|
{/* Previous Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => navigateImage(-1)}
|
||||||
|
disabled={selectedImageIndex <= 0}
|
||||||
|
className="p-3 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={24} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
<img
|
<img
|
||||||
src={selectedImage.url}
|
src={selectedImage.url}
|
||||||
alt={selectedImage.plate}
|
alt={selectedImage.plate}
|
||||||
className="max-w-full max-h-[60vh] rounded-lg border border-slate-700"
|
className="max-w-[70%] max-h-[55vh] rounded-lg border border-slate-700"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Next Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => navigateImage(1)}
|
||||||
|
disabled={selectedImageIndex >= datasetImages.length - 1}
|
||||||
|
className="p-3 bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plate Info */}
|
||||||
<div className="bg-slate-800 px-4 py-2 rounded-lg">
|
<div className="bg-slate-800 px-4 py-2 rounded-lg">
|
||||||
<span className="text-slate-400">Patente: </span>
|
<span className="text-slate-400">Patente: </span>
|
||||||
<span className="font-mono font-bold text-emerald-400 text-lg">{selectedImage.plate}</span>
|
<span className="font-mono font-bold text-emerald-400 text-lg">{selectedImage.plate}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Keyboard hint */}
|
||||||
|
<div className="text-slate-500 text-xs">
|
||||||
|
Usa ← → para navegar
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Image Grid */
|
/* Image Grid */
|
||||||
@@ -563,7 +684,7 @@ function AdminDashboard({ token }) {
|
|||||||
{datasetImages.map((img, idx) => (
|
{datasetImages.map((img, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => setSelectedImage(img)}
|
onClick={() => selectImageByIndex(idx)}
|
||||||
className="cursor-pointer group relative aspect-video bg-slate-800 rounded-lg overflow-hidden border border-slate-700 hover:border-emerald-500 transition-all"
|
className="cursor-pointer group relative aspect-video bg-slate-800 rounded-lg overflow-hidden border border-slate-700 hover:border-emerald-500 transition-all"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
Reference in New Issue
Block a user