Skip to content

Commit

Permalink
feat(Conversation): added feature to view and download images, DONE
Browse files Browse the repository at this point in the history
  • Loading branch information
0xfrankz committed Sep 5, 2024
1 parent 72aec9f commit e47e0c0
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7", features = [ "sqlite", "runtime-tokio", "tls-native-tls" ] }
strum = "0.26"
strum_macros = "0.26"
tauri = { version = "1.5.4", features = [ "clipboard-write-text", "clipboard-read-text", "updater", "shell-open", "path-all", "fs-write-file", "fs-remove-dir", "fs-read-dir", "fs-exists", "fs-copy-file", "fs-read-file", "fs-rename-file", "fs-create-dir", "fs-remove-file"] }
tauri = { version = "1.5.4", features = [ "dialog-save", "clipboard-write-text", "clipboard-read-text", "updater", "shell-open", "path-all", "fs-write-file", "fs-remove-dir", "fs-read-dir", "fs-exists", "fs-copy-file", "fs-read-file", "fs-rename-file", "fs-create-dir", "fs-remove-file"] }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
thiserror = "1.0"
tokio = "1.36.0"
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"clipboard": {
"writeText": true,
"readText": true
},
"dialog": {
"save": true
}
},
"bundle": {
Expand Down
82 changes: 72 additions & 10 deletions src/components/ImagePreviewer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { save } from '@tauri-apps/api/dialog';
import { writeBinaryFile } from '@tauri-apps/api/fs';
import { downloadDir } from '@tauri-apps/api/path';
import { useHover } from 'ahooks';
import { X } from 'lucide-react';
import { Save, X } from 'lucide-react';
import { forwardRef, type HtmlHTMLAttributes, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';

import log from '@/lib/log';
import type { FileData } from '@/lib/types';
import { cn } from '@/lib/utils';

Expand All @@ -13,7 +19,18 @@ import {
CarouselNext,
CarouselPrevious,
} from './ui/carousel';
import { Dialog, DialogContent, DialogTitle } from './ui/dialog';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from './ui/context-menu';
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from './ui/dialog';

type ImagePreviewerProps = {
files: FileData[];
Expand Down Expand Up @@ -43,7 +60,7 @@ export function ImageThumbnail({
<div className="relative size-12" ref={ref}>
<button
type="button"
className="m-0 border-0 bg-transparent p-0"
className="m-0 size-full border-0 bg-transparent p-0"
onClick={onClick}
aria-label={`View ${imageData.fileName}`}
>
Expand All @@ -70,6 +87,27 @@ export const ImagePreviwer = forwardRef<
HtmlHTMLAttributes<HTMLDivElement> & ImagePreviewerProps
>(({ className, files, deletable = true, onDelete }, ref) => {
const [open, setOpen] = useState(false);
const { t } = useTranslation();

const onSaveAsClick = async (imageData: FileData) => {
const downloadDirPath = await downloadDir();
const filePath = await save({
defaultPath: `${downloadDirPath}/${imageData.fileName}`,
});
if (filePath) {
try {
await writeBinaryFile({ path: filePath, contents: imageData.fileData });
toast.success(t('generic:message:image-saved-as', { path: filePath }));
} catch (error: unknown) {
if (error instanceof Error) {
await log.error(`Error saving file: ${error.message}`);
} else {
await log.error(`Error saving file: ${String(error)}`);
}
toast.error(t('error:image-save-error'));
}
}
};
return (
<div className={cn('w-full', className)} ref={ref}>
<ul className="m-0 flex min-h-12 list-none gap-2 p-0">
Expand All @@ -88,23 +126,47 @@ export const ImagePreviwer = forwardRef<
})}
</ul>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogContent className="max-w-fit px-20">
<DialogTitle className="hidden">Images Carousel</DialogTitle>
<Carousel className="w-full max-w-xs">
<CarouselContent>
<DialogDescription className="hidden">
{files.length} images
</DialogDescription>
<Carousel className="w-full max-w-lg">
<CarouselContent className="my-6">
{files.map((f, idx) => {
const key = `carousel_${f.fileName}_${idx}`;
const blob = new Blob([f.fileData], { type: f.fileType });
const imageSrc = URL.createObjectURL(blob);
return (
<CarouselItem key={key}>
<img src={imageSrc} alt={f.fileName} />
<CarouselItem key={key} className="flex">
<ContextMenu>
<ContextMenuTrigger className="m-auto size-fit">
<img
src={imageSrc}
alt={f.fileName}
className="max-h-96 max-w-lg"
/>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
className="cursor-pointer gap-2"
onClick={() => onSaveAsClick(f)}
>
<Save className="size-4" />
{t('generic:action:save-as')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</CarouselItem>
);
})}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
{files.length > 1 ? (
<>
<CarouselPrevious />
<CarouselNext />
</>
) : null}
</Carousel>
</DialogContent>
</Dialog>
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/en/error.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"empty-prompt": "You prompt is blank. Blank prompt is a waste of your tokens quota.",
"empty-api-key": "API key cannot be empty.",
"empty-endpoint": "Endpoint cannot be empty."
}
},
"image-save-error": "Failed to save image"
}
4 changes: 3 additions & 1 deletion src/i18n/locales/en/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"delete": "Delete",
"edit": "Edit",
"save": "Save",
"save-as": "Save as",
"use": "Use",
"set": "Set",
"retry": "Retry",
Expand Down Expand Up @@ -57,7 +58,8 @@
"release-to-upload": "Release to upload",
"max-upload-files-warning": "You can only upload a maximum of {{maxNumOfUploadFiles}} files",
"n-more-conversations": "{{n}} more conversations",
"copy-of": "Copy of {{original}}"
"copy-of": "Copy of {{original}}",
"image-saved-as": "Image saved as {{path}}"
},
"label": {
"default": "Default",
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/zh-Hans/error.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"empty-prompt": "你输入了空的提示词。空提示词只会浪费你的接口资源。",
"empty-api-key": "API密钥不能为空。",
"empty-endpoint": "Endpoint不能为空。"
}
},
"image-save-error": "图片保存失败"
}
4 changes: 3 additions & 1 deletion src/i18n/locales/zh-Hans/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"delete": "删除",
"edit": "编辑",
"save": "保存",
"save-as": "另存为",
"use": "使用",
"set": "设置",
"retry": "重试",
Expand Down Expand Up @@ -57,7 +58,8 @@
"release-to-upload": "松手进行上传",
"max-upload-files-warning": "仅可上传最多{{maxNumOfUploadFiles}}个文件",
"n-more-conversations": "剩余{{n}}条对话",
"copy-of": "{{original}}的副本"
"copy-of": "{{original}}的副本",
"image-saved-as": "图片已保存为{{path}}"
},
"label": {
"default": "默认",
Expand Down

0 comments on commit e47e0c0

Please # to comment.