mirror of
https://github.com/get-drexa/drive.git
synced 2025-12-01 05:51:39 +00:00
feat: initial impl of file proxy
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
# this is the url to the convex instance (NOT THE DASHBOARD)
|
||||
VITE_CONVEX_URL=
|
||||
# this is the convex url for invoking http actions
|
||||
VITE_CONVEX_SITE_URL=
|
||||
VITE_CONVEX_SITE_URL=
|
||||
# this is the url to the file proxy
|
||||
FILE_PROXY_URL=
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { Doc } from "@fileone/convex/dataModel"
|
||||
import type { OpenedFile } from "@fileone/convex/filesystem"
|
||||
import { ImagePreviewDialog } from "./image-preview-dialog"
|
||||
|
||||
export function FilePreviewDialog({
|
||||
file,
|
||||
openedFile,
|
||||
onClose,
|
||||
}: {
|
||||
file: Doc<"files">
|
||||
openedFile: OpenedFile
|
||||
onClose: () => void
|
||||
}) {
|
||||
if (!file) return null
|
||||
|
||||
switch (file.mimeType) {
|
||||
switch (openedFile.file.mimeType) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
case "image/gif":
|
||||
return <ImagePreviewDialog file={file} onClose={onClose} />
|
||||
return (
|
||||
<ImagePreviewDialog openedFile={openedFile} onClose={onClose} />
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
3
apps/drive-web/src/files/file-share.ts
Normal file
3
apps/drive-web/src/files/file-share.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function fileShareUrl(shareToken: string) {
|
||||
return `${import.meta.env.VITE_FILE_PROXY_URL}/files/${shareToken}`
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { api } from "@fileone/convex/api"
|
||||
import type { Doc } from "@fileone/convex/dataModel"
|
||||
import type { OpenedFile } from "@fileone/convex/filesystem"
|
||||
import { DialogTitle } from "@radix-ui/react-dialog"
|
||||
import { useQuery as useConvexQuery } from "convex/react"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
|
||||
import {
|
||||
DownloadIcon,
|
||||
@@ -18,9 +16,8 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
} from "@/components/ui/dialog"
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner"
|
||||
import { fileShareUrl } from "./file-share"
|
||||
|
||||
const zoomLevelAtom = atom(
|
||||
1,
|
||||
@@ -35,15 +32,12 @@ const zoomLevelAtom = atom(
|
||||
)
|
||||
|
||||
export function ImagePreviewDialog({
|
||||
file,
|
||||
openedFile,
|
||||
onClose,
|
||||
}: {
|
||||
file: Doc<"files">
|
||||
openedFile: OpenedFile
|
||||
onClose: () => void
|
||||
}) {
|
||||
const fileUrl = useConvexQuery(api.filesystem.fetchFileUrl, {
|
||||
fileId: file._id,
|
||||
})
|
||||
const setZoomLevel = useSetAtom(zoomLevelAtom)
|
||||
|
||||
useEffect(
|
||||
@@ -62,23 +56,12 @@ export function ImagePreviewDialog({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogOverlay className="flex items-center justify-center">
|
||||
{!fileUrl ? (
|
||||
<LoadingSpinner className="text-neutral-200 size-10" />
|
||||
) : null}
|
||||
</DialogOverlay>
|
||||
{fileUrl ? <PreviewContent fileUrl={fileUrl} file={file} /> : null}
|
||||
<PreviewContent openedFile={openedFile} />
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewContent({
|
||||
fileUrl,
|
||||
file,
|
||||
}: {
|
||||
fileUrl: string
|
||||
file: Doc<"files">
|
||||
}) {
|
||||
function PreviewContent({ openedFile }: { openedFile: OpenedFile }) {
|
||||
return (
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
@@ -86,10 +69,10 @@ function PreviewContent({
|
||||
>
|
||||
<DialogHeader className="overflow-auto border-b border-b-border p-4 flex flex-row items-center justify-between">
|
||||
<DialogTitle className="truncate flex-1">
|
||||
{file.name}
|
||||
{openedFile.file.name}
|
||||
</DialogTitle>
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Toolbar fileUrl={fileUrl} file={file} />
|
||||
<Toolbar openedFile={openedFile} />
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<DialogClose>
|
||||
<XIcon />
|
||||
@@ -99,13 +82,13 @@ function PreviewContent({
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="w-full h-full flex items-center justify-center max-h-[calc(100vh-10rem)] overflow-auto">
|
||||
<ImagePreview fileUrl={fileUrl} file={file} />
|
||||
<ImagePreview openedFile={openedFile} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
function Toolbar({ fileUrl, file }: { fileUrl: string; file: Doc<"files"> }) {
|
||||
function Toolbar({ openedFile }: { openedFile: OpenedFile }) {
|
||||
const setZoomLevel = useSetAtom(zoomLevelAtom)
|
||||
const zoomInterval = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
@@ -159,8 +142,8 @@ function Toolbar({ fileUrl, file }: { fileUrl: string; file: Doc<"files"> }) {
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={file.name}
|
||||
href={fileShareUrl(openedFile.shareToken)}
|
||||
download={openedFile.file.name}
|
||||
target="_blank"
|
||||
className="flex flex-row items-center"
|
||||
>
|
||||
@@ -191,18 +174,12 @@ function ResetZoomButton() {
|
||||
)
|
||||
}
|
||||
|
||||
function ImagePreview({
|
||||
fileUrl,
|
||||
file,
|
||||
}: {
|
||||
fileUrl: string
|
||||
file: Doc<"files">
|
||||
}) {
|
||||
function ImagePreview({ openedFile }: { openedFile: OpenedFile }) {
|
||||
const zoomLevel = useAtomValue(zoomLevelAtom)
|
||||
return (
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={file.name}
|
||||
src={fileShareUrl(openedFile.shareToken)}
|
||||
alt={openedFile.file.name}
|
||||
className="object-contain"
|
||||
style={{ transform: `scale(${zoomLevel})` }}
|
||||
/>
|
||||
|
||||
@@ -3,12 +3,14 @@ import type { Doc, Id } from "@fileone/convex/dataModel"
|
||||
import {
|
||||
type FileSystemItem,
|
||||
newFileSystemHandle,
|
||||
type OpenedFile,
|
||||
} from "@fileone/convex/filesystem"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import type { Row, Table } from "@tanstack/react-table"
|
||||
import {
|
||||
useMutation as useContextMutation,
|
||||
useMutation as useConvexMutation,
|
||||
useQuery as useConvexQuery,
|
||||
} from "convex/react"
|
||||
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||
@@ -77,22 +79,12 @@ const fileDragInfoAtom = atom<FileDragInfo | null>(null)
|
||||
const optimisticDeletedItemsAtom = atom(
|
||||
new Set<Id<"files"> | Id<"directories">>(),
|
||||
)
|
||||
const openedFileAtom = atom<Doc<"files"> | null>(null)
|
||||
const openedFileAtom = atom<OpenedFile | null>(null)
|
||||
const itemBeingRenamedAtom = atom<{
|
||||
originalItem: FileSystemItem
|
||||
name: string
|
||||
} | null>(null)
|
||||
|
||||
const tableFilterAtom = atom((get) => {
|
||||
const optimisticDeletedItems = get(optimisticDeletedItemsAtom)
|
||||
console.log("optimisticDeletedItems", optimisticDeletedItems)
|
||||
return (item: FileSystemItem) => {
|
||||
const test = !optimisticDeletedItems.has(item.doc._id)
|
||||
console.log("test", test)
|
||||
return test
|
||||
}
|
||||
})
|
||||
|
||||
// MARK: page entry
|
||||
function RouteComponent() {
|
||||
const { directoryId } = Route.useParams()
|
||||
@@ -100,7 +92,6 @@ function RouteComponent() {
|
||||
const directory = useConvexQuery(api.files.fetchDirectory, {
|
||||
directoryId,
|
||||
})
|
||||
const store = useStore()
|
||||
const directoryContent = useConvexQuery(
|
||||
api.filesystem.fetchDirectoryContent,
|
||||
{
|
||||
@@ -108,45 +99,12 @@ function RouteComponent() {
|
||||
trashed: false,
|
||||
},
|
||||
)
|
||||
const setOpenedFile = useSetAtom(openedFileAtom)
|
||||
const setContextMenuTargetItems = useSetAtom(contextMenuTargetItemsAtom)
|
||||
|
||||
const tableFilter = useCallback(
|
||||
(item: FileSystemItem) =>
|
||||
store.get(optimisticDeletedItemsAtom).has(item.doc._id),
|
||||
[store],
|
||||
)
|
||||
|
||||
const openFile = useCallback(
|
||||
(file: Doc<"files">) => {
|
||||
setOpenedFile(file)
|
||||
},
|
||||
[setOpenedFile],
|
||||
)
|
||||
|
||||
const directoryUrlFn = useCallback(
|
||||
(directory: Doc<"directories">) => `/directories/${directory._id}`,
|
||||
[],
|
||||
)
|
||||
|
||||
const directoryUrlById = useCallback(
|
||||
(directoryId: Id<"directories">) => `/directories/${directoryId}`,
|
||||
[],
|
||||
)
|
||||
|
||||
const handleContextMenuRequest = (
|
||||
row: Row<FileSystemItem>,
|
||||
table: Table<FileSystemItem>,
|
||||
) => {
|
||||
if (row.getIsSelected()) {
|
||||
setContextMenuTargetItems(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
)
|
||||
} else {
|
||||
setContextMenuTargetItems([row.original])
|
||||
}
|
||||
}
|
||||
|
||||
if (!directory || !directoryContent || !rootDirectory) {
|
||||
return <DirectoryPageSkeleton />
|
||||
}
|
||||
@@ -171,17 +129,7 @@ function RouteComponent() {
|
||||
{/* DirectoryContentContextMenu must wrap div instead of DirectoryContentTable, otherwise radix will throw "event.preventDefault is not a function" error, idk why */}
|
||||
<DirectoryContentContextMenu>
|
||||
<div className="w-full">
|
||||
<WithAtom atom={optimisticDeletedItemsAtom}>
|
||||
{(optimisticDeletedItems) => (
|
||||
<DirectoryContentTable
|
||||
hiddenItems={optimisticDeletedItems}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
fileDragInfoAtom={fileDragInfoAtom}
|
||||
onContextMenu={handleContextMenuRequest}
|
||||
onOpenFile={openFile}
|
||||
/>
|
||||
)}
|
||||
</WithAtom>
|
||||
<_DirectoryContentTable />
|
||||
</div>
|
||||
</DirectoryContentContextMenu>
|
||||
|
||||
@@ -228,7 +176,7 @@ function RouteComponent() {
|
||||
if (!openedFile) return null
|
||||
return (
|
||||
<FilePreviewDialog
|
||||
file={openedFile}
|
||||
openedFile={openedFile}
|
||||
onClose={() => setOpenedFile(null)}
|
||||
/>
|
||||
)
|
||||
@@ -238,6 +186,58 @@ function RouteComponent() {
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: directory table
|
||||
|
||||
function _DirectoryContentTable() {
|
||||
const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom)
|
||||
const setOpenedFile = useSetAtom(openedFileAtom)
|
||||
const setContextMenuTargetItems = useSetAtom(contextMenuTargetItemsAtom)
|
||||
|
||||
const { mutate: openFile } = useMutation({
|
||||
mutationFn: useConvexMutation(api.filesystem.openFile),
|
||||
onSuccess: (openedFile: OpenedFile) => {
|
||||
console.log("openedFile", openedFile)
|
||||
setOpenedFile(openedFile)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
toast.error("Failed to open file")
|
||||
},
|
||||
})
|
||||
|
||||
const onTableOpenFile = (file: Doc<"files">) => {
|
||||
openFile({ fileId: file._id })
|
||||
}
|
||||
|
||||
const directoryUrlFn = useCallback(
|
||||
(directory: Doc<"directories">) => `/directories/${directory._id}`,
|
||||
[],
|
||||
)
|
||||
|
||||
const handleContextMenuRequest = (
|
||||
row: Row<FileSystemItem>,
|
||||
table: Table<FileSystemItem>,
|
||||
) => {
|
||||
if (row.getIsSelected()) {
|
||||
setContextMenuTargetItems(
|
||||
table.getSelectedRowModel().rows.map((row) => row.original),
|
||||
)
|
||||
} else {
|
||||
setContextMenuTargetItems([row.original])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DirectoryContentTable
|
||||
hiddenItems={optimisticDeletedItems}
|
||||
directoryUrlFn={directoryUrlFn}
|
||||
fileDragInfoAtom={fileDragInfoAtom}
|
||||
onContextMenu={handleContextMenuRequest}
|
||||
onOpenFile={onTableOpenFile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// ==================================
|
||||
// MARK: ctx menu
|
||||
|
||||
|
||||
Reference in New Issue
Block a user