Skip to content

Commit

Permalink
feat: Image thumbnail preview in grid view (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
NriotHrreion committed Aug 7, 2024
1 parent fe9734c commit 72a2cfd
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 15 deletions.
44 changes: 44 additions & 0 deletions app/api/fs/thumbnail/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from "node:fs";

import { NextRequest, NextResponse } from "next/server";
import mime from "mime";

import { tokenStorageKey } from "@/lib/global";
import { validateToken } from "@/lib/token";
import { error } from "@/lib/packet";
import { getExtname, getFileType } from "@/lib/utils";
import { streamFile } from "@/lib/stream";

export async function GET(req: NextRequest) {
const token = req.cookies.get(tokenStorageKey)?.value;

if(!token) return error(401);
if(!validateToken(token)) return error(403);

const { searchParams } = new URL(req.url);
const targetPath = searchParams.get("path") ?? "/";

try {
if(!targetPath || !fs.existsSync(targetPath)) return error(404);

const stat = fs.statSync(targetPath);

if(!stat.isFile() || getFileType(getExtname(targetPath))?.id !== "image") return error(400);

const stream = fs.createReadStream(targetPath);

return new NextResponse(streamFile(stream), {
status: 200,
headers: {
"Content-Disposition": `attachment; filename=thumbnail`,
"Content-Type": mime.getType(targetPath) ?? "application/octet-stream",
"Content-Length": stat.size.toString()
}
});
} catch (err) {
// eslint-disable-next-line no-console
console.log("[Server: /api/fs/thumbnail] "+ err);

return error(500);
}
}
14 changes: 12 additions & 2 deletions components/explorer/explorer-grid-view-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import { Tooltip } from "@nextui-org/tooltip";

import { getFileIcon, getFolderIcon } from "./explorer-item";

import { formatSize, getFileTypeName } from "@/lib/utils";
import { concatPath, formatSize, getFileType, getFileTypeName } from "@/lib/utils";
import { useExplorer } from "@/hooks/useExplorer";

interface GridViewItemProps extends ViewItemProps {}

const ExplorerGridViewItem: React.FC<GridViewItemProps> = ({
extname, size, selected, contextMenu, setSelected, handleSelection, handleOpen, onContextMenu, ...props
}) => {
const explorer = useExplorer();

return (
<div
className="w-[6.5rem] h-28 flex flex-col items-center overflow-hidden relative"
Expand Down Expand Up @@ -48,7 +51,14 @@ const ExplorerGridViewItem: React.FC<GridViewItemProps> = ({
{
props.type === "folder"
? getFolderIcon(props.name, 34)
: getFileIcon(extname ?? "", 34)
: (
getFileType(extname ?? "")?.id === "image"
? <img
className="max-w-[50px] max-h-16"
src={`/api/fs/thumbnail?path=${explorer.disk + concatPath(explorer.stringifyPath(), props.name)}`}
alt="thumbnail"/>
: getFileIcon(extname ?? "", 34)
)
}
</div>
</Tooltip>
Expand Down
4 changes: 2 additions & 2 deletions components/explorer/explorer-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import ExplorerListViewItem from "./explorer-list-view-item";
import ExplorerGridViewItem from "./explorer-grid-view-item";

import { useExplorer } from "@/hooks/useExplorer";
import { concatPath, getFileType } from "@/lib/utils";
import { concatPath, getExtname, getFileType } from "@/lib/utils";
import { getViewer } from "@/lib/viewers";
import { useDialog } from "@/hooks/useDialog";
import { useFile } from "@/hooks/useFile";
Expand Down Expand Up @@ -93,7 +93,7 @@ interface ExplorerItemProps extends DirectoryItem {
}

const ExplorerItem: React.FC<ExplorerItemProps> = ({ displayingMode, ...props }) => {
const extname = useMemo(() => props.name.split(".").findLast(() => true), [props.name]);
const extname = getExtname(props.name);

const [selected, setSelected] = useState<boolean>(false);

Expand Down
16 changes: 15 additions & 1 deletion components/explorer/explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { useExplorer } from "@/hooks/useExplorer";
import { useDialog } from "@/hooks/useDialog";
import { useForceUpdate } from "@/hooks/useForceUpdate";
import { useEmitter } from "@/hooks/useEmitter";
import { getExtname, getFileType } from "@/lib/utils";
import { emitter } from "@/lib/emitter";

interface FolderResponseData extends BaseResponseData {
items: DirectoryItem[]
Expand All @@ -35,13 +37,25 @@ const Explorer: React.FC = () => {
.then(({ data }) => {
var list: DirectoryItem[] = [];

// Classify the directory items
// And count the amount of image files
var imageCount = 0;
data.items.forEach((item) => {
if(item.type === "folder") list.push(item);
});
data.items.forEach((item) => {
if(item.type === "file") list.push(item);
if(item.type === "file") {
list.push(item);
if(getFileType(getExtname(item.name))?.id === "image") {
imageCount++;
}
}
});

const defaultDisplayingMode = imageCount >= 5 ? "grid" : "list";
explorer.displayingMode = defaultDisplayingMode;
emitter.emit("displaying-mode-change", defaultDisplayingMode);

setItems(list);
})
.catch((err: AxiosError) => {
Expand Down
13 changes: 10 additions & 3 deletions components/explorer/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import type { DisplayingMode } from "@/types";

import React, { useEffect, useCallback, useState, useMemo } from "react";
import { Breadcrumbs, BreadcrumbItem } from "@nextui-org/breadcrumbs";
import { Input } from "@nextui-org/input";
Expand All @@ -15,8 +17,9 @@ import DiskItem from "./disk-item";

import { parseStringPath, useExplorer } from "@/hooks/useExplorer";
import { useFerrum } from "@/hooks/useFerrum";
import { concatPath, isValidPath } from "@/lib/utils";
import { concatPath, getExtname, isValidPath } from "@/lib/utils";
import { emitter } from "@/lib/emitter";
import { useEmitter } from "@/hooks/useEmitter";

const Navbar: React.FC = () => {
const ferrum = useFerrum();
Expand Down Expand Up @@ -77,9 +80,13 @@ const Navbar: React.FC = () => {

useEffect(() => {
explorer.displayingMode = displayingMode;
emitter.emit("displaying-mode-change");
emitter.emit("displaying-mode-change", displayingMode);
}, [displayingMode]);

useEmitter([
["displaying-mode-change", (current: DisplayingMode) => setDisplayingMode(current)]
]);

return (
<>
<div className="w-[1000px] flex gap-2 mt-2">
Expand Down Expand Up @@ -191,7 +198,7 @@ const Navbar: React.FC = () => {
<BreadcrumbItem
classNames={{ item: "gap-1" }}
isCurrent>
{getFileIcon(explorer.currentViewing.split(".").findLast(() => true) ?? "")}
{getFileIcon(getExtname(explorer.currentViewing))}
{explorer.currentViewing}
</BreadcrumbItem>
)
Expand Down
3 changes: 2 additions & 1 deletion components/viewers/audio-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Viewer, { ViewerProps } from ".";
import PlayIcon from "@/styles/icons/play.svg";
import PauseIcon from "@/styles/icons/pause.svg";
import StopIcon from "@/styles/icons/stop.svg";
import { getExtname } from "@/lib/utils";

interface Lyric {
time: number
Expand Down Expand Up @@ -119,7 +120,7 @@ export default class AudioViewer extends Viewer<AudioViewerProps, AudioViewerSta
alt={this.state.metadata?.common.picture[0].description}/>
: (
<div className="w-full h-full flex justify-center items-center bg-default-100 rounded-lg">
<span className="text-4xl font-bold text-default-400">{this.props.fileName.split(".").findLast(() => true)?.toUpperCase()}</span>
<span className="text-4xl font-bold text-default-400">{getExtname(this.props.fileName).toUpperCase()}</span>
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion hooks/useDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export const useDialog = create<DialogStore>((set) => ({
type: null,
data: null,
isOpened: false,
open: (type: DialogType, data?: any) => set({ type, data, isOpened: true }),
open: (type, data?) => set({ type, data, isOpened: true }),
close: () => set({ type: null, data: null, isOpened: false }),
}));
10 changes: 8 additions & 2 deletions hooks/useExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { to } from "preps";

import { storage } from "@/lib/storage";
import { diskStorageKey } from "@/lib/global";
import { emitter } from "@/lib/emitter";

interface ExplorerStore {
path: string[]
Expand All @@ -15,6 +16,7 @@ interface ExplorerStore {
setPath: (path: string[]) => void
setDisk: (disk: string) => void
setCurrentViewing: (file: string) => void
setDisplayingMode: (displayingMode: DisplayingMode) => void
clearCurrentViewing: () => void
stringifyPath: () => string
enterPath: (target: string) => void
Expand Down Expand Up @@ -46,14 +48,18 @@ export const useExplorer = create<ExplorerStore>((set, get) => ({
displayingMode: "list",

setPath: (path) => set({ path }),
setDisk: (disk: string) => {
setDisk: (disk) => {
set({ disk });
storage.setItem(diskStorageKey, disk);
},
setCurrentViewing: (file) => set({ currentViewing: file }),
setDisplayingMode: (displayingMode) => {
set({ displayingMode });
emitter.emit("displaying-mode-change", displayingMode);
},
clearCurrentViewing: () => set({ currentViewing: null }),
stringifyPath: () => stringifyPath(get().path),
enterPath: (target: string) => {
enterPath: (target) => {
set({ path: [...get().path, target] });
},
backToRoot: () => {
Expand Down
2 changes: 1 addition & 1 deletion hooks/useFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function useFile(path: string): FileOperations {
const fullPath = explorer.disk + path;

return {
rename: async (newName: string) => {
rename: async (newName) => {
if(/[\\\/:*?"<>|]/.test(newName)) {
toast.warn(`文件名称中不能包含下列任何字符 \\ / : * ? " < > |`);

Expand Down
4 changes: 2 additions & 2 deletions hooks/useFolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function useFolder(fullPath: string): FolderOperations {
const explorer = useExplorer();

return {
rename: async (newName: string) => {
rename: async (newName) => {
if(/[\\\/:*?"<>|]/.test(newName)) {
toast.warn(`文件夹名称中不能包含下列任何字符 \\ / : * ? " < > |`);

Expand Down Expand Up @@ -107,7 +107,7 @@ export function useFolder(fullPath: string): FolderOperations {
}
});
},
create: async (name: string, type: "folder" | "file") => {
create: async (name, type) => {
const typeName = type === "folder" ? "文件夹" : "文件";

if(/[\\\/:*?"<>|]/.test(name)) {
Expand Down
4 changes: 4 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export function formatSize(bytes: number, fixed: number = 2): string {
return size.value + getBytesType(size.type);
}

export function getExtname(fileName: string): string {
return fileName.split(".").findLast(() => true) ?? "";
}

export function getFileType(extname: string): FileType | null {
extname = extname.toLowerCase();

Expand Down

0 comments on commit 72a2cfd

Please sign in to comment.