mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-04 05:53:23 +00:00 
			
		
		
		
	Compare commits
	
		
			12 Commits
		
	
	
		
			v3.0.0-bet
			...
			v3.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cd14c28be1 | ||
| 
						 | 
					3c084a6686 | ||
| 
						 | 
					6a1381684b | ||
| 
						 | 
					dc20770fe6 | ||
| 
						 | 
					6e526f7f88 | ||
| 
						 | 
					858852c8cd | ||
| 
						 | 
					363dedbb2c | ||
| 
						 | 
					cd215c79b8 | ||
| 
						 | 
					98586efbcd | ||
| 
						 | 
					c724e644c7 | ||
| 
						 | 
					555ff18a87 | ||
| 
						 | 
					5100e1591b | 
@@ -4,7 +4,21 @@ import { FastifyReply, FastifyRequest } from "fastify";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
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)) {
 | 
			
		||||
  fs.mkdirSync(uploadsDir, { recursive: true });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import { env } from "../../env";
 | 
			
		||||
import { ConfigService } from "../config/service";
 | 
			
		||||
import nodemailer from "nodemailer";
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +13,7 @@ export class EmailService {
 | 
			
		||||
    return nodemailer.createTransport({
 | 
			
		||||
      host: await this.configService.getValue("smtpHost"),
 | 
			
		||||
      port: Number(await this.configService.getValue("smtpPort")),
 | 
			
		||||
      secure: env.SECURE_SITE === "true" ? true : false,
 | 
			
		||||
      secure: false,
 | 
			
		||||
      auth: {
 | 
			
		||||
        user: await this.configService.getValue("smtpUser"),
 | 
			
		||||
        pass: await this.configService.getValue("smtpPass"),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { env } from "../env";
 | 
			
		||||
import { StorageProvider } from "../types/storage";
 | 
			
		||||
import { IS_RUNNING_IN_CONTAINER } from "../utils/container-detection";
 | 
			
		||||
import * as crypto from "crypto";
 | 
			
		||||
import * as fsSync from "fs";
 | 
			
		||||
import * as fs from "fs/promises";
 | 
			
		||||
@@ -9,12 +10,14 @@ import { pipeline } from "stream/promises";
 | 
			
		||||
 | 
			
		||||
export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
  private static instance: FilesystemStorageProvider;
 | 
			
		||||
  private uploadsDir = path.join(process.cwd(), "uploads");
 | 
			
		||||
  private uploadsDir: string;
 | 
			
		||||
  private encryptionKey = env.ENCRYPTION_KEY;
 | 
			
		||||
  private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
 | 
			
		||||
  private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
 | 
			
		||||
 | 
			
		||||
  private constructor() {
 | 
			
		||||
    this.uploadsDir = IS_RUNNING_IN_CONTAINER ? "/app/server/uploads" : path.join(process.cwd(), "uploads");
 | 
			
		||||
 | 
			
		||||
    this.ensureUploadsDir();
 | 
			
		||||
    setInterval(() => this.cleanExpiredTokens(), 5 * 60 * 1000);
 | 
			
		||||
  }
 | 
			
		||||
@@ -214,8 +217,10 @@ export class FilesystemStorageProvider implements StorageProvider {
 | 
			
		||||
    if (encryptedBuffer.length > 16) {
 | 
			
		||||
      try {
 | 
			
		||||
        return this.decryptFileBuffer(encryptedBuffer);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.warn("Failed to decrypt with new method, trying legacy format");
 | 
			
		||||
      } catch (error: unknown) {
 | 
			
		||||
        if (error instanceof Error) {
 | 
			
		||||
          console.warn("Failed to decrypt with new method, trying legacy format", error.message);
 | 
			
		||||
        }
 | 
			
		||||
        return this.decryptFileLegacy(encryptedBuffer);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import { reverseShareRoutes } from "./modules/reverse-share/routes";
 | 
			
		||||
import { shareRoutes } from "./modules/share/routes";
 | 
			
		||||
import { storageRoutes } from "./modules/storage/routes";
 | 
			
		||||
import { userRoutes } from "./modules/user/routes";
 | 
			
		||||
import { IS_RUNNING_IN_CONTAINER } from "./utils/container-detection";
 | 
			
		||||
import fastifyMultipart from "@fastify/multipart";
 | 
			
		||||
import fastifyStatic from "@fastify/static";
 | 
			
		||||
import * as fs from "fs/promises";
 | 
			
		||||
@@ -26,21 +27,22 @@ if (typeof global.crypto === "undefined") {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function ensureDirectories() {
 | 
			
		||||
  const uploadsDir = path.join(process.cwd(), "uploads");
 | 
			
		||||
  const tempChunksDir = path.join(process.cwd(), "temp-chunks");
 | 
			
		||||
  const baseDir = IS_RUNNING_IN_CONTAINER ? "/app/server" : process.cwd();
 | 
			
		||||
  const uploadsDir = path.join(baseDir, "uploads");
 | 
			
		||||
  const tempChunksDir = path.join(baseDir, "temp-chunks");
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await fs.access(uploadsDir);
 | 
			
		||||
  } catch {
 | 
			
		||||
    await fs.mkdir(uploadsDir, { recursive: true });
 | 
			
		||||
    console.log("📁 Created uploads directory");
 | 
			
		||||
    console.log(`📁 Created uploads directory: ${uploadsDir}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await fs.access(tempChunksDir);
 | 
			
		||||
  } catch {
 | 
			
		||||
    await fs.mkdir(tempChunksDir, { recursive: true });
 | 
			
		||||
    console.log("📁 Created temp-chunks directory");
 | 
			
		||||
    console.log(`📁 Created temp-chunks directory: ${tempChunksDir}`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -62,8 +64,11 @@ async function startServer() {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (env.ENABLE_S3 !== "true") {
 | 
			
		||||
    const baseDir = IS_RUNNING_IN_CONTAINER ? "/app/server" : process.cwd();
 | 
			
		||||
    const uploadsPath = path.join(baseDir, "uploads");
 | 
			
		||||
 | 
			
		||||
    await app.register(fastifyStatic, {
 | 
			
		||||
      root: path.join(process.cwd(), "uploads"),
 | 
			
		||||
      root: uploadsPath,
 | 
			
		||||
      prefix: "/uploads/",
 | 
			
		||||
      decorateReply: false,
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								apps/server/src/utils/container-detection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								apps/server/src/utils/container-detection.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import * as fsSync from "fs";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Determines if the application is running inside a container environment.
 | 
			
		||||
 * Checks common container indicators like /.dockerenv and cgroup file patterns.
 | 
			
		||||
 *
 | 
			
		||||
 * This function caches its result after the first call for performance.
 | 
			
		||||
 *
 | 
			
		||||
 * @returns {boolean} True if running in a container, false otherwise.
 | 
			
		||||
 */
 | 
			
		||||
function isRunningInContainer(): boolean {
 | 
			
		||||
  try {
 | 
			
		||||
    if (fsSync.existsSync("/.dockerenv")) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const cgroupContent = fsSync.readFileSync("/proc/self/cgroup", "utf8");
 | 
			
		||||
    const containerPatterns = [
 | 
			
		||||
      "docker",
 | 
			
		||||
      "containerd",
 | 
			
		||||
      "lxc",
 | 
			
		||||
      "kubepods",
 | 
			
		||||
      "pod",
 | 
			
		||||
      "/containers/",
 | 
			
		||||
      "system.slice/container-",
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const pattern of containerPatterns) {
 | 
			
		||||
      if (cgroupContent.includes(pattern)) {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fsSync.existsSync("/.well-known/container")) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e: unknown) {
 | 
			
		||||
    if (e instanceof Error) {
 | 
			
		||||
      console.warn("Could not perform full container detection:", e.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const IS_RUNNING_IN_CONTAINER = isRunningInContainer();
 | 
			
		||||
@@ -2,12 +2,12 @@ import { useState } from "react";
 | 
			
		||||
import { IconDownload, IconEye } from "@tabler/icons-react";
 | 
			
		||||
import { useTranslations } from "next-intl";
 | 
			
		||||
 | 
			
		||||
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
 | 
			
		||||
import { getFileIcon } from "@/utils/file-icons";
 | 
			
		||||
import { formatFileSize } from "@/utils/format-file-size";
 | 
			
		||||
import { ShareFilesTableProps } from "../types";
 | 
			
		||||
import { ShareFilePreviewModal } from "./share-file-preview-modal";
 | 
			
		||||
 | 
			
		||||
export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
 | 
			
		||||
  const t = useTranslations();
 | 
			
		||||
@@ -99,7 +99,14 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
 | 
			
		||||
        </Table>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {selectedFile && <FilePreviewModal isOpen={isPreviewOpen} onClose={handleClosePreview} file={selectedFile} />}
 | 
			
		||||
      {selectedFile && (
 | 
			
		||||
        <ShareFilePreviewModal
 | 
			
		||||
          isOpen={isPreviewOpen}
 | 
			
		||||
          onClose={handleClosePreview}
 | 
			
		||||
          file={selectedFile}
 | 
			
		||||
          onDownload={onDownload}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </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 }> }) {
 | 
			
		||||
  const cookieHeader = req.headers.get("cookie");
 | 
			
		||||
  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",
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
      cookie: cookieHeader || "",
 | 
			
		||||
    },
 | 
			
		||||
    body: body,
 | 
			
		||||
    redirect: "manual",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user