mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-04 05:53:23 +00:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			v3.0.0-bet
			...
			v3.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6a1381684b | ||
| 
						 | 
					dc20770fe6 | ||
| 
						 | 
					6e526f7f88 | ||
| 
						 | 
					858852c8cd | ||
| 
						 | 
					363dedbb2c | ||
| 
						 | 
					cd215c79b8 | ||
| 
						 | 
					98586efbcd | ||
| 
						 | 
					c724e644c7 | ||
| 
						 | 
					555ff18a87 | ||
| 
						 | 
					5100e1591b | 
@@ -4,7 +4,21 @@ import { FastifyReply, FastifyRequest } from "fastify";
 | 
				
			|||||||
import fs from "fs";
 | 
					import fs from "fs";
 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const uploadsDir = path.join(process.cwd(), "uploads/logo");
 | 
					const isDocker = (() => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    require("fs").statSync("/.dockerenv");
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  } catch {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return require("fs").readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const baseDir = isDocker ? "/app/server" : process.cwd();
 | 
				
			||||||
 | 
					const uploadsDir = path.join(baseDir, "uploads/logo");
 | 
				
			||||||
if (!fs.existsSync(uploadsDir)) {
 | 
					if (!fs.existsSync(uploadsDir)) {
 | 
				
			||||||
  fs.mkdirSync(uploadsDir, { recursive: true });
 | 
					  fs.mkdirSync(uploadsDir, { recursive: true });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import { env } from "../../env";
 | 
					 | 
				
			||||||
import { ConfigService } from "../config/service";
 | 
					import { ConfigService } from "../config/service";
 | 
				
			||||||
import nodemailer from "nodemailer";
 | 
					import nodemailer from "nodemailer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,7 +13,7 @@ export class EmailService {
 | 
				
			|||||||
    return nodemailer.createTransport({
 | 
					    return nodemailer.createTransport({
 | 
				
			||||||
      host: await this.configService.getValue("smtpHost"),
 | 
					      host: await this.configService.getValue("smtpHost"),
 | 
				
			||||||
      port: Number(await this.configService.getValue("smtpPort")),
 | 
					      port: Number(await this.configService.getValue("smtpPort")),
 | 
				
			||||||
      secure: env.SECURE_SITE === "true" ? true : false,
 | 
					      secure: false,
 | 
				
			||||||
      auth: {
 | 
					      auth: {
 | 
				
			||||||
        user: await this.configService.getValue("smtpUser"),
 | 
					        user: await this.configService.getValue("smtpUser"),
 | 
				
			||||||
        pass: await this.configService.getValue("smtpPass"),
 | 
					        pass: await this.configService.getValue("smtpPass"),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,16 +9,31 @@ import { pipeline } from "stream/promises";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export class FilesystemStorageProvider implements StorageProvider {
 | 
					export class FilesystemStorageProvider implements StorageProvider {
 | 
				
			||||||
  private static instance: FilesystemStorageProvider;
 | 
					  private static instance: FilesystemStorageProvider;
 | 
				
			||||||
  private uploadsDir = path.join(process.cwd(), "uploads");
 | 
					  private uploadsDir: string;
 | 
				
			||||||
  private encryptionKey = env.ENCRYPTION_KEY;
 | 
					  private encryptionKey = env.ENCRYPTION_KEY;
 | 
				
			||||||
  private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
 | 
					  private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
 | 
				
			||||||
  private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
 | 
					  private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private constructor() {
 | 
					  private constructor() {
 | 
				
			||||||
 | 
					    this.uploadsDir = this.isDocker() ? "/app/server/uploads" : path.join(process.cwd(), "uploads");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.ensureUploadsDir();
 | 
					    this.ensureUploadsDir();
 | 
				
			||||||
    setInterval(() => this.cleanExpiredTokens(), 5 * 60 * 1000);
 | 
					    setInterval(() => this.cleanExpiredTokens(), 5 * 60 * 1000);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private isDocker(): boolean {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      fsSync.statSync("/.dockerenv");
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return fsSync.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static getInstance(): FilesystemStorageProvider {
 | 
					  public static getInstance(): FilesystemStorageProvider {
 | 
				
			||||||
    if (!FilesystemStorageProvider.instance) {
 | 
					    if (!FilesystemStorageProvider.instance) {
 | 
				
			||||||
      FilesystemStorageProvider.instance = new FilesystemStorageProvider();
 | 
					      FilesystemStorageProvider.instance = new FilesystemStorageProvider();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ import { storageRoutes } from "./modules/storage/routes";
 | 
				
			|||||||
import { userRoutes } from "./modules/user/routes";
 | 
					import { userRoutes } from "./modules/user/routes";
 | 
				
			||||||
import fastifyMultipart from "@fastify/multipart";
 | 
					import fastifyMultipart from "@fastify/multipart";
 | 
				
			||||||
import fastifyStatic from "@fastify/static";
 | 
					import fastifyStatic from "@fastify/static";
 | 
				
			||||||
 | 
					import * as fsSync from "fs";
 | 
				
			||||||
import * as fs from "fs/promises";
 | 
					import * as fs from "fs/promises";
 | 
				
			||||||
import crypto from "node:crypto";
 | 
					import crypto from "node:crypto";
 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
@@ -26,21 +27,36 @@ if (typeof global.crypto === "undefined") {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function ensureDirectories() {
 | 
					async function ensureDirectories() {
 | 
				
			||||||
  const uploadsDir = path.join(process.cwd(), "uploads");
 | 
					  // Use /app/server paths in Docker, current directory for local development
 | 
				
			||||||
  const tempChunksDir = path.join(process.cwd(), "temp-chunks");
 | 
					  const isDocker = (() => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      fsSync.statSync("/.dockerenv");
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return fsSync.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const baseDir = isDocker ? "/app/server" : process.cwd();
 | 
				
			||||||
 | 
					  const uploadsDir = path.join(baseDir, "uploads");
 | 
				
			||||||
 | 
					  const tempChunksDir = path.join(baseDir, "temp-chunks");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    await fs.access(uploadsDir);
 | 
					    await fs.access(uploadsDir);
 | 
				
			||||||
  } catch {
 | 
					  } catch {
 | 
				
			||||||
    await fs.mkdir(uploadsDir, { recursive: true });
 | 
					    await fs.mkdir(uploadsDir, { recursive: true });
 | 
				
			||||||
    console.log("📁 Created uploads directory");
 | 
					    console.log(`📁 Created uploads directory: ${uploadsDir}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    await fs.access(tempChunksDir);
 | 
					    await fs.access(tempChunksDir);
 | 
				
			||||||
  } catch {
 | 
					  } catch {
 | 
				
			||||||
    await fs.mkdir(tempChunksDir, { recursive: true });
 | 
					    await fs.mkdir(tempChunksDir, { recursive: true });
 | 
				
			||||||
    console.log("📁 Created temp-chunks directory");
 | 
					    console.log(`📁 Created temp-chunks directory: ${tempChunksDir}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,8 +78,24 @@ async function startServer() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (env.ENABLE_S3 !== "true") {
 | 
					  if (env.ENABLE_S3 !== "true") {
 | 
				
			||||||
 | 
					    const isDocker = (() => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        fsSync.statSync("/.dockerenv");
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          return fsSync.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					          return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const baseDir = isDocker ? "/app/server" : process.cwd();
 | 
				
			||||||
 | 
					    const uploadsPath = path.join(baseDir, "uploads");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await app.register(fastifyStatic, {
 | 
					    await app.register(fastifyStatic, {
 | 
				
			||||||
      root: path.join(process.cwd(), "uploads"),
 | 
					      root: uploadsPath,
 | 
				
			||||||
      prefix: "/uploads/",
 | 
					      prefix: "/uploads/",
 | 
				
			||||||
      decorateReply: false,
 | 
					      decorateReply: false,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,12 +2,12 @@ import { useState } from "react";
 | 
				
			|||||||
import { IconDownload, IconEye } from "@tabler/icons-react";
 | 
					import { IconDownload, IconEye } from "@tabler/icons-react";
 | 
				
			||||||
import { useTranslations } from "next-intl";
 | 
					import { useTranslations } from "next-intl";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
 | 
					 | 
				
			||||||
import { Button } from "@/components/ui/button";
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
 | 
					import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
 | 
				
			||||||
import { getFileIcon } from "@/utils/file-icons";
 | 
					import { getFileIcon } from "@/utils/file-icons";
 | 
				
			||||||
import { formatFileSize } from "@/utils/format-file-size";
 | 
					import { formatFileSize } from "@/utils/format-file-size";
 | 
				
			||||||
import { ShareFilesTableProps } from "../types";
 | 
					import { ShareFilesTableProps } from "../types";
 | 
				
			||||||
 | 
					import { ShareFilePreviewModal } from "./share-file-preview-modal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
 | 
					export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
 | 
				
			||||||
  const t = useTranslations();
 | 
					  const t = useTranslations();
 | 
				
			||||||
@@ -99,7 +99,14 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
 | 
				
			|||||||
        </Table>
 | 
					        </Table>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {selectedFile && <FilePreviewModal isOpen={isPreviewOpen} onClose={handleClosePreview} file={selectedFile} />}
 | 
					      {selectedFile && (
 | 
				
			||||||
 | 
					        <ShareFilePreviewModal
 | 
				
			||||||
 | 
					          isOpen={isPreviewOpen}
 | 
				
			||||||
 | 
					          onClose={handleClosePreview}
 | 
				
			||||||
 | 
					          file={selectedFile}
 | 
				
			||||||
 | 
					          onDownload={onDownload}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,320 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { IconDownload } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import { useTranslations } from "next-intl";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CustomAudioPlayer } from "@/components/audio/custom-audio-player";
 | 
				
			||||||
 | 
					import { AspectRatio } from "@/components/ui/aspect-ratio";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { ScrollArea } from "@/components/ui/scroll-area";
 | 
				
			||||||
 | 
					import { getDownloadUrl } from "@/http/endpoints";
 | 
				
			||||||
 | 
					import { getFileIcon } from "@/utils/file-icons";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ShareFilePreviewModalProps {
 | 
				
			||||||
 | 
					  isOpen: boolean;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					  file: {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    objectName: string;
 | 
				
			||||||
 | 
					    type?: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  onDownload: (objectName: string, fileName: string) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ShareFilePreviewModal({ isOpen, onClose, file, onDownload }: ShareFilePreviewModalProps) {
 | 
				
			||||||
 | 
					  const t = useTranslations();
 | 
				
			||||||
 | 
					  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(true);
 | 
				
			||||||
 | 
					  const [videoBlob, setVideoBlob] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const [pdfAsBlob, setPdfAsBlob] = useState(false);
 | 
				
			||||||
 | 
					  const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const [pdfLoadFailed, setPdfLoadFailed] = useState(false);
 | 
				
			||||||
 | 
					  const [isLoadingPreview, setIsLoadingPreview] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (isOpen && file.objectName && !isLoadingPreview) {
 | 
				
			||||||
 | 
					      setIsLoading(true);
 | 
				
			||||||
 | 
					      setPreviewUrl(null);
 | 
				
			||||||
 | 
					      setVideoBlob(null);
 | 
				
			||||||
 | 
					      setPdfAsBlob(false);
 | 
				
			||||||
 | 
					      setDownloadUrl(null);
 | 
				
			||||||
 | 
					      setPdfLoadFailed(false);
 | 
				
			||||||
 | 
					      loadPreview();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [file.objectName, isOpen]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      if (previewUrl && previewUrl.startsWith("blob:")) {
 | 
				
			||||||
 | 
					        URL.revokeObjectURL(previewUrl);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (videoBlob && videoBlob.startsWith("blob:")) {
 | 
				
			||||||
 | 
					        URL.revokeObjectURL(videoBlob);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [previewUrl, videoBlob]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!isOpen) {
 | 
				
			||||||
 | 
					      if (previewUrl && previewUrl.startsWith("blob:")) {
 | 
				
			||||||
 | 
					        URL.revokeObjectURL(previewUrl);
 | 
				
			||||||
 | 
					        setPreviewUrl(null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (videoBlob && videoBlob.startsWith("blob:")) {
 | 
				
			||||||
 | 
					        URL.revokeObjectURL(videoBlob);
 | 
				
			||||||
 | 
					        setVideoBlob(null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [isOpen]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadPreview = async () => {
 | 
				
			||||||
 | 
					    if (!file.objectName || isLoadingPreview) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setIsLoadingPreview(true);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const encodedObjectName = encodeURIComponent(file.objectName);
 | 
				
			||||||
 | 
					      const response = await getDownloadUrl(encodedObjectName);
 | 
				
			||||||
 | 
					      const url = response.data.url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setDownloadUrl(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const fileType = getFileType();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (fileType === "video") {
 | 
				
			||||||
 | 
					        await loadVideoPreview(url);
 | 
				
			||||||
 | 
					      } else if (fileType === "audio") {
 | 
				
			||||||
 | 
					        await loadAudioPreview(url);
 | 
				
			||||||
 | 
					      } else if (fileType === "pdf") {
 | 
				
			||||||
 | 
					        await loadPdfPreview(url);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setPreviewUrl(url);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error("Failed to load preview:", error);
 | 
				
			||||||
 | 
					      toast.error(t("filePreview.loadError"));
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsLoading(false);
 | 
				
			||||||
 | 
					      setIsLoadingPreview(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadVideoPreview = async (url: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch(url);
 | 
				
			||||||
 | 
					      if (!response.ok) {
 | 
				
			||||||
 | 
					        throw new Error(`HTTP error! status: ${response.status}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const blob = await response.blob();
 | 
				
			||||||
 | 
					      const blobUrl = URL.createObjectURL(blob);
 | 
				
			||||||
 | 
					      setVideoBlob(blobUrl);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error("Failed to load video as blob:", error);
 | 
				
			||||||
 | 
					      setPreviewUrl(url);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadAudioPreview = async (url: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch(url);
 | 
				
			||||||
 | 
					      if (!response.ok) {
 | 
				
			||||||
 | 
					        throw new Error(`HTTP error! status: ${response.status}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const blob = await response.blob();
 | 
				
			||||||
 | 
					      const blobUrl = URL.createObjectURL(blob);
 | 
				
			||||||
 | 
					      setPreviewUrl(blobUrl);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error("Failed to load audio as blob:", error);
 | 
				
			||||||
 | 
					      setPreviewUrl(url);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadPdfPreview = async (url: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch(url);
 | 
				
			||||||
 | 
					      if (!response.ok) {
 | 
				
			||||||
 | 
					        throw new Error(`HTTP error! status: ${response.status}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const blob = await response.blob();
 | 
				
			||||||
 | 
					      const finalBlob = new Blob([blob], { type: "application/pdf" });
 | 
				
			||||||
 | 
					      const blobUrl = URL.createObjectURL(finalBlob);
 | 
				
			||||||
 | 
					      setPreviewUrl(blobUrl);
 | 
				
			||||||
 | 
					      setPdfAsBlob(true);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error("Failed to load PDF as blob:", error);
 | 
				
			||||||
 | 
					      setPreviewUrl(url);
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        if (!pdfLoadFailed && !pdfAsBlob) {
 | 
				
			||||||
 | 
					          handlePdfLoadError();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }, 4000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handlePdfLoadError = async () => {
 | 
				
			||||||
 | 
					    if (pdfLoadFailed || pdfAsBlob) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setPdfLoadFailed(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (downloadUrl) {
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        loadPdfPreview(downloadUrl);
 | 
				
			||||||
 | 
					      }, 500);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDownload = () => {
 | 
				
			||||||
 | 
					    onDownload(file.objectName, file.name);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getFileType = () => {
 | 
				
			||||||
 | 
					    const extension = file.name.split(".").pop()?.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (extension === "pdf") return "pdf";
 | 
				
			||||||
 | 
					    if (["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "tiff"].includes(extension || "")) return "image";
 | 
				
			||||||
 | 
					    if (["mp3", "wav", "ogg", "m4a", "aac", "flac"].includes(extension || "")) return "audio";
 | 
				
			||||||
 | 
					    if (["mp4", "webm", "ogg", "mov", "avi", "mkv", "wmv", "flv", "m4v"].includes(extension || "")) return "video";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "other";
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const renderPreview = () => {
 | 
				
			||||||
 | 
					    const fileType = getFileType();
 | 
				
			||||||
 | 
					    const { icon: FileIcon, color } = getFileIcon(file.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isLoading) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className="flex flex-col items-center justify-center h-96 gap-4">
 | 
				
			||||||
 | 
					          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
 | 
				
			||||||
 | 
					          <p className="text-muted-foreground">{t("filePreview.loading")}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mediaUrl = fileType === "video" ? videoBlob : previewUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!mediaUrl && (fileType === "video" || fileType === "audio")) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className="flex flex-col items-center justify-center h-96 gap-4">
 | 
				
			||||||
 | 
					          <FileIcon className={`h-12 w-12 ${color}`} />
 | 
				
			||||||
 | 
					          <p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
 | 
				
			||||||
 | 
					          <p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!previewUrl && fileType !== "video") {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className="flex flex-col items-center justify-center h-96 gap-4">
 | 
				
			||||||
 | 
					          <FileIcon className={`h-12 w-12 ${color}`} />
 | 
				
			||||||
 | 
					          <p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
 | 
				
			||||||
 | 
					          <p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (fileType) {
 | 
				
			||||||
 | 
					      case "pdf":
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <ScrollArea className="w-full">
 | 
				
			||||||
 | 
					            <div className="w-full min-h-[600px] border rounded-lg overflow-hidden bg-card">
 | 
				
			||||||
 | 
					              {pdfAsBlob ? (
 | 
				
			||||||
 | 
					                <iframe
 | 
				
			||||||
 | 
					                  src={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
 | 
				
			||||||
 | 
					                  className="w-full h-full min-h-[600px]"
 | 
				
			||||||
 | 
					                  title={file.name}
 | 
				
			||||||
 | 
					                  style={{ border: "none" }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              ) : pdfLoadFailed ? (
 | 
				
			||||||
 | 
					                <div className="flex items-center justify-center h-full min-h-[600px]">
 | 
				
			||||||
 | 
					                  <div className="flex flex-col items-center gap-4">
 | 
				
			||||||
 | 
					                    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
 | 
				
			||||||
 | 
					                    <p className="text-muted-foreground">{t("filePreview.loadingAlternative")}</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <div className="w-full h-full min-h-[600px] relative">
 | 
				
			||||||
 | 
					                  <object
 | 
				
			||||||
 | 
					                    data={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
 | 
				
			||||||
 | 
					                    type="application/pdf"
 | 
				
			||||||
 | 
					                    className="w-full h-full min-h-[600px]"
 | 
				
			||||||
 | 
					                    onError={handlePdfLoadError}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <iframe
 | 
				
			||||||
 | 
					                      src={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
 | 
				
			||||||
 | 
					                      className="w-full h-full min-h-[600px]"
 | 
				
			||||||
 | 
					                      title={file.name}
 | 
				
			||||||
 | 
					                      style={{ border: "none" }}
 | 
				
			||||||
 | 
					                      onError={handlePdfLoadError}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </object>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </ScrollArea>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "image":
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <AspectRatio ratio={16 / 9} className="bg-muted">
 | 
				
			||||||
 | 
					            <img src={previewUrl!} alt={file.name} className="object-contain w-full h-full rounded-md" />
 | 
				
			||||||
 | 
					          </AspectRatio>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "audio":
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div className="flex flex-col items-center justify-center gap-6 py-4">
 | 
				
			||||||
 | 
					            <CustomAudioPlayer src={mediaUrl!} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      case "video":
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div className="flex flex-col items-center justify-center gap-4 py-6">
 | 
				
			||||||
 | 
					            <div className="w-full max-w-4xl">
 | 
				
			||||||
 | 
					              <video controls className="w-full rounded-lg" preload="metadata" style={{ maxHeight: "70vh" }}>
 | 
				
			||||||
 | 
					                <source src={mediaUrl!} />
 | 
				
			||||||
 | 
					                {t("filePreview.videoNotSupported")}
 | 
				
			||||||
 | 
					              </video>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div className="flex flex-col items-center justify-center h-96 gap-4">
 | 
				
			||||||
 | 
					            <FileIcon className={`text-6xl ${color}`} />
 | 
				
			||||||
 | 
					            <p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
 | 
				
			||||||
 | 
					            <p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog open={isOpen} onOpenChange={onClose}>
 | 
				
			||||||
 | 
					      <DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
 | 
				
			||||||
 | 
					        <DialogHeader>
 | 
				
			||||||
 | 
					          <DialogTitle className="flex items-center gap-2">
 | 
				
			||||||
 | 
					            {(() => {
 | 
				
			||||||
 | 
					              const FileIcon = getFileIcon(file.name).icon;
 | 
				
			||||||
 | 
					              return <FileIcon size={24} />;
 | 
				
			||||||
 | 
					            })()}
 | 
				
			||||||
 | 
					            <span className="truncate">{file.name}</span>
 | 
				
			||||||
 | 
					          </DialogTitle>
 | 
				
			||||||
 | 
					        </DialogHeader>
 | 
				
			||||||
 | 
					        <div className="flex-1 overflow-auto">{renderPreview()}</div>
 | 
				
			||||||
 | 
					        <DialogFooter>
 | 
				
			||||||
 | 
					          <Button variant="outline" onClick={onClose}>
 | 
				
			||||||
 | 
					            {t("common.close")}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					          <Button onClick={handleDownload}>
 | 
				
			||||||
 | 
					            <IconDownload className="h-4 w-4" />
 | 
				
			||||||
 | 
					            {t("common.download")}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </DialogFooter>
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,12 +3,15 @@ import { NextRequest, NextResponse } from "next/server";
 | 
				
			|||||||
export async function POST(req: NextRequest, { params }: { params: Promise<{ shareId: string }> }) {
 | 
					export async function POST(req: NextRequest, { params }: { params: Promise<{ shareId: string }> }) {
 | 
				
			||||||
  const cookieHeader = req.headers.get("cookie");
 | 
					  const cookieHeader = req.headers.get("cookie");
 | 
				
			||||||
  const { shareId } = await params;
 | 
					  const { shareId } = await params;
 | 
				
			||||||
 | 
					  const body = await req.text();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/${shareId}/recipients/notify`, {
 | 
					  const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/${shareId}/notify`, {
 | 
				
			||||||
    method: "POST",
 | 
					    method: "POST",
 | 
				
			||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
      cookie: cookieHeader || "",
 | 
					      cookie: cookieHeader || "",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    body: body,
 | 
				
			||||||
    redirect: "manual",
 | 
					    redirect: "manual",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user