Skip to content

Commit

Permalink
Various improvements (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
KennethWussmann authored Nov 5, 2024
1 parent 136de34 commit e90075b
Show file tree
Hide file tree
Showing 17 changed files with 538 additions and 320 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const CaptionListDropdown = () => {
const { copy, paste, hasContent } = useCaptionClipboard();
return (
<DropdownMenu >
<DropdownMenuTrigger>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"}><EllipsisVertical /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
Expand Down
63 changes: 63 additions & 0 deletions src/components/common/double-confirmation-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState, cloneElement, ReactElement } from "react";
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle, DialogDescription, Button, DialogFooter } from "../ui";

type DoubleConfirmationDialogProps = {
title?: string;
message?: string;
children: ReactElement;
};

export const DoubleConfirmationDialog = ({
title = "Are you sure?",
message = "This action cannot be undone.",
children,
}: DoubleConfirmationDialogProps) => {
const [isOpen, setOpen] = useState(false);
const [onConfirm, setOnConfirm] = useState<(() => void) | null>(null);

const handleClick = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();

if (children.props.onClick) {
setOnConfirm(() => children.props.onClick);
}

setOpen(true);
};

const handleConfirm = () => {
if (onConfirm) onConfirm();
setOpen(false);
};

const handleCancel = () => {
setOpen(false);
};

return (
<Dialog open={isOpen} onOpenChange={setOpen}>
<DialogTrigger asChild>
{cloneElement(children, { onClick: handleClick })}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
{message}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<div className="flex flex-row gap-4 justify-between">
<Button variant="ghost" onClick={handleCancel}>
Cancel
</Button>
<Button variant="destructive" onClick={handleConfirm}>
Confirm
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useDatasetDirectory } from "@/hooks/provider/dataset-directory-provider
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
import { deleteAllIndexedDBs } from "@/lib/utils";
import { DoubleConfirmationDialog } from "@/components/common/double-confirmation-dialog";

const AdvancedSettingsContent = () => {
const { deleteAllTextFiles } = useDatasetDirectory();
Expand Down Expand Up @@ -60,9 +61,11 @@ const AdvancedSettingsContent = () => {
</div>
</TableCell>
<TableCell className="text-right">
<Button variant={"destructive"} onClick={resetSettings}>
Reset
</Button>
<DoubleConfirmationDialog message="This cannot be undone! All your settings will be removed.">
<Button variant={"destructive"} onClick={resetSettings}>
Reset
</Button>
</DoubleConfirmationDialog>
</TableCell>
</TableRow>
<TableRow>
Expand All @@ -75,9 +78,11 @@ const AdvancedSettingsContent = () => {
</div>
</TableCell>
<TableCell className="text-right">
<Button variant={"destructive"} onClick={deleteDatabase}>
Permanently delete database
</Button>
<DoubleConfirmationDialog message={`This cannot be undone! All your labels data you entered via ${productName} will be lost if you didn't export them!`}>
<Button variant={"destructive"} onClick={deleteDatabase}>
Permanently delete database
</Button>
</DoubleConfirmationDialog>
</TableCell>
</TableRow>
<TableRow>
Expand All @@ -90,9 +95,11 @@ const AdvancedSettingsContent = () => {
</div>
</TableCell>
<TableCell className="text-right">
<Button variant={"destructive"} onClick={deleteTextFiles} disabled={isDeletingAllTextFiles}>
Permanently delete text files
</Button>
<DoubleConfirmationDialog title="Are you sure?" message="This cannot be undone. This will remove all files in your selected dataset directory that have the file extension .txt. This will not affect any other files in the directory.">
<Button variant={"destructive"} onClick={deleteTextFiles} disabled={isDeletingAllTextFiles}>
Permanently delete text files
</Button>
</DoubleConfirmationDialog>
</TableCell>
</TableRow>
</TableBody>
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/provider/caption-editor-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { usePreventClose } from "./prevent-close-provider";
import { uuid } from "@/lib/utils";
import { usePrevious } from "@uidotdev/usehooks"
import { useShortcut } from "../use-shortcut";
import { database } from "@/lib/database/database";
import { useCaptionPreview } from "../use-caption-preview";
import { useDatabase } from "@/lib/database/database-provider";

interface CaptionEditorContextType {
parts: CaptionPart[];
Expand Down Expand Up @@ -44,6 +44,7 @@ interface CaptionEditorProviderProps {
export const CaptionEditorProvider: React.FC<CaptionEditorProviderProps> = ({
children,
}) => {
const { database } = useDatabase()
const { currentImage } = useImageNavigation();
const [isDirty, setDirty] = useState(false);
const [parts, setParts] = useState<CaptionPart[]>([]);
Expand Down
19 changes: 11 additions & 8 deletions src/hooks/provider/dataset-directory-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ImageFile, TextFile } from "@/lib/types";

type DatasetDirectoryContextType = {
supported: boolean;
openDirectoryPicker: () => Promise<void>;
openDirectoryPicker: () => Promise<FileSystemDirectoryHandle | null>;
isDirectorySelected: boolean;
imageFiles: ImageFile[];
textFiles: TextFile[];
Expand All @@ -25,6 +25,7 @@ type DatasetDirectoryContextType = {
writeCaption: (caption: string, image: ImageFile) => Promise<void>;
deleteAllTextFiles: () => Promise<void>;
directoryHandle: FileSystemDirectoryHandle | null;
loadDirectory: (handle: FileSystemDirectoryHandle) => Promise<ImageFile[]>;
};

const DatasetDirectoryContext = createContext<
Expand Down Expand Up @@ -66,7 +67,7 @@ export const DatasetDirectoryProvider = ({
setAccessDenied(false);
}, []);

const loadDirectory = useCallback(
const loadDirectory =
async (handle: FileSystemDirectoryHandle) => {
setDirectoryLoaded(false);
setImageFiles([]);
Expand Down Expand Up @@ -138,9 +139,9 @@ export const DatasetDirectoryProvider = ({
setFailedImageFiles(failedImagesCount);
setFailedTextFiles(orphanTextFilesCount);
setDirectoryLoaded(true);
},
[]
);

return finalImageFiles
}

const openDirectoryPicker = useCallback(async () => {
if (!supported || !window.showDirectoryPicker) {
Expand All @@ -152,13 +153,14 @@ export const DatasetDirectoryProvider = ({
const dirHandle = await window.showDirectoryPicker({ mode: "readwrite" });
setDirectoryHandle(dirHandle);
setIsDirectorySelected(true);
await loadDirectory(dirHandle);
return dirHandle;
} catch (err) {
console.error("Error opening directory:", err);
resetState();
setAccessDenied(true);
}
}, [supported, loadDirectory, resetState]);
return null
}, [supported, resetState]);

const writeTextFile = useCallback(
async (filePath: string, content: string) => {
Expand Down Expand Up @@ -313,7 +315,8 @@ export const DatasetDirectoryProvider = ({
loadImage,
writeCaption,
directoryHandle,
deleteAllTextFiles
deleteAllTextFiles,
loadDirectory
};

return (
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/provider/image-navigation-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useImages } from "../use-images";
import { useShortcut } from "../use-shortcut";
import { ImageEntity } from "@/lib/database/image-entity";
import { useLiveQuery } from "dexie-react-hooks";
import { database } from "@/lib/database/database";
import { useDatabase } from "@/lib/database/database-provider";

interface ImageNavigationContextType {
currentImage?: ImageEntity;
Expand All @@ -17,6 +17,7 @@ interface ImageNavigationContextType {
const ImageNavigationContext = createContext<ImageNavigationContextType | undefined>(undefined);

export const ImageNavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { database } = useDatabase()
const { images } = useImages();
const [currentImageId, setCurrentImageId] = useState<string>();
const [hasNextImage, setHasNextImage] = useState(false);
Expand Down
78 changes: 39 additions & 39 deletions src/hooks/use-image-importer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,64 @@ import { useAtom } from "jotai/react";
import { useDatasetDirectory } from "./provider/dataset-directory-provider";
import { getFilenameWithoutExtension } from "@/lib/utils";
import { useState } from "react";
import { useImages } from "./use-images";
import { ImageEntity } from "@/lib/database/image-entity";
import { database } from "@/lib/database/database";
import { useDatabase } from "@/lib/database/database-provider";
import { Database } from "@/lib/database/database";

export const useImageImporter = () => {
const { setAutoBackupEnabled } = useDatabase()
const [separator] = useAtom(settings.caption.separator);
const { directoryHandle } = useDatasetDirectory();
const { allImages } = useImages()
const [imported, setImported] = useState(false)

const importImages = (imageFiles: ImageFile[]) => {
const importImages = async (
database: Database,
existingImages: ImageEntity[],
imageFiles: ImageFile[]
) => {
if (!directoryHandle || imported) {
return
}
(async () => {
const imageDocs = await Promise.all(imageFiles.map(async (image): Promise<ImageEntity | null> => {
const existingImage = allImages.find(existingImage => existingImage.filename === image.name)
let caption: Caption | null = null
try {
const captionFileHandle = await directoryHandle.getFileHandle(getFilenameWithoutExtension(image.name) + ".txt")
const file = await captionFileHandle.getFile()
const captionText = await file.text()
const imageDocs = await Promise.all(imageFiles.map(async (image): Promise<ImageEntity | null> => {
const existingImage = existingImages.find(existingImage => existingImage.filename === image.name)
let caption: Caption | null = null
try {
const captionFileHandle = await directoryHandle.getFileHandle(getFilenameWithoutExtension(image.name) + ".txt")
const file = await captionFileHandle.getFile()
const captionText = await file.text()

if (captionText && captionText.trim().length > 0) {
caption = {
parts: (captionText.split(separator).map(part => part.trim()) || []).filter(part => part.length > 0).map((text, index) => ({ id: index.toString(), text, index })),
preview: captionText
}
if (captionText && captionText.trim().length > 0) {
caption = {
parts: (captionText.split(separator).map(part => part.trim()) || []).filter(part => part.length > 0).map((text, index) => ({ id: index.toString(), text, index })),
preview: captionText
}
} catch {
// No caption file
}
} catch {
// No caption file
}

if (caption) {
return {
id: image.name,
filename: image.name,
caption: caption.preview,
captionParts: caption.parts
}
} else if (existingImage) {
return existingImage
} else {
return {
id: image.name,
filename: image.name,
}
if (caption) {
return {
id: image.name,
filename: image.name,
caption: caption.preview,
captionParts: caption.parts
}
} else if (existingImage) {
return null
} else {
return {
id: image.name,
filename: image.name,
}
}))
}
}))

await database.images.bulkPut(imageDocs.filter(doc => doc !== null))
await database.images.bulkPut(imageDocs.filter(doc => doc !== null))

setImported(true)
setAutoBackupEnabled(true)
console.log("Imported images")
})()
setImported(true)
setAutoBackupEnabled(true)
console.log("Imported images")
}

return { importImages, imported }
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/use-images.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { settings } from "@/lib/settings";
import { useAtom } from "jotai/react";
import { useAction } from "./use-action";
import { isImageDone } from "@/lib/database/image-entity";
import { database } from "@/lib/database/database";
import { useLiveQuery } from "dexie-react-hooks";
import { useDatabase } from "@/lib/database/database-provider";

export const useImages = () => {
const action = useAction()
const [hideDone] = useAtom(settings.appearance.hideDoneImages)
const { database } = useDatabase()

const allImages = useLiveQuery(() => database.images.toArray())
const doneImages = action && allImages ? allImages.filter(image => isImageDone(image, action)) : []
Expand Down
Loading

0 comments on commit e90075b

Please sign in to comment.