mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat: add functionality to embed uploaded images with BBCode or HTML (#296)
This commit is contained in:
@@ -585,6 +585,51 @@ export class FileController {
|
||||
}
|
||||
}
|
||||
|
||||
async embedFile(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params as { id: string };
|
||||
|
||||
if (!id) {
|
||||
return reply.status(400).send({ error: "File ID is required." });
|
||||
}
|
||||
|
||||
const fileRecord = await prisma.file.findUnique({ where: { id } });
|
||||
|
||||
if (!fileRecord) {
|
||||
return reply.status(404).send({ error: "File not found." });
|
||||
}
|
||||
|
||||
const extension = fileRecord.extension.toLowerCase();
|
||||
const imageExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"];
|
||||
const videoExts = ["mp4", "webm", "ogg", "mov", "avi", "mkv", "flv", "wmv"];
|
||||
const audioExts = ["mp3", "wav", "ogg", "m4a", "flac", "aac", "wma"];
|
||||
|
||||
const isMedia = imageExts.includes(extension) || videoExts.includes(extension) || audioExts.includes(extension);
|
||||
|
||||
if (!isMedia) {
|
||||
return reply.status(403).send({
|
||||
error: "Embed is only allowed for images, videos, and audio files.",
|
||||
});
|
||||
}
|
||||
|
||||
const storageProvider = (this.fileService as any).storageProvider;
|
||||
const filePath = storageProvider.getFilePath(fileRecord.objectName);
|
||||
|
||||
const contentType = getContentType(fileRecord.name);
|
||||
const fileName = fileRecord.name;
|
||||
|
||||
reply.header("Content-Type", contentType);
|
||||
reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`);
|
||||
reply.header("Cache-Control", "public, max-age=31536000"); // Cache por 1 ano
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
return reply.send(stream);
|
||||
} catch (error) {
|
||||
console.error("Error in embedFile:", error);
|
||||
return reply.status(500).send({ error: "Internal server error." });
|
||||
}
|
||||
}
|
||||
|
||||
private async getAllUserFilesRecursively(userId: string): Promise<any[]> {
|
||||
const rootFiles = await prisma.file.findMany({
|
||||
where: { userId, folderId: null },
|
||||
|
@@ -131,6 +131,29 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
fileController.getDownloadUrl.bind(fileController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/embed/:id",
|
||||
{
|
||||
schema: {
|
||||
tags: ["File"],
|
||||
operationId: "embedFile",
|
||||
summary: "Embed File (Public Access)",
|
||||
description:
|
||||
"Returns a media file (image/video/audio) for public embedding without authentication. Only works for media files.",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "File ID is required").describe("The file ID"),
|
||||
}),
|
||||
response: {
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
403: z.object({ error: z.string().describe("Error message - not a media file") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
500: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
fileController.embedFile.bind(fileController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/files/download",
|
||||
{
|
||||
|
@@ -150,7 +150,9 @@
|
||||
"move": "نقل",
|
||||
"rename": "إعادة تسمية",
|
||||
"search": "بحث",
|
||||
"share": "مشاركة"
|
||||
"share": "مشاركة",
|
||||
"copied": "تم النسخ",
|
||||
"copy": "نسخ"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "إنشاء مشاركة",
|
||||
@@ -1933,5 +1935,17 @@
|
||||
"passwordRequired": "كلمة المرور مطلوبة",
|
||||
"nameRequired": "الاسم مطلوب",
|
||||
"required": "هذا الحقل مطلوب"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "تضمين الصورة",
|
||||
"description": "استخدم هذه الأكواد لتضمين هذه الصورة في المنتديات أو المواقع الإلكترونية أو المنصات الأخرى",
|
||||
"tabs": {
|
||||
"directLink": "رابط مباشر",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "عنوان URL مباشر لملف الصورة",
|
||||
"htmlDescription": "استخدم هذا الكود لتضمين الصورة في صفحات HTML",
|
||||
"bbcodeDescription": "استخدم هذا الكود لتضمين الصورة في المنتديات التي تدعم BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Verschieben",
|
||||
"rename": "Umbenennen",
|
||||
"search": "Suchen",
|
||||
"share": "Teilen"
|
||||
"share": "Teilen",
|
||||
"copied": "Kopiert",
|
||||
"copy": "Kopieren"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Freigabe Erstellen",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "Passwort ist erforderlich",
|
||||
"nameRequired": "Name ist erforderlich",
|
||||
"required": "Dieses Feld ist erforderlich"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Bild einbetten",
|
||||
"description": "Verwenden Sie diese Codes, um dieses Bild in Foren, Websites oder anderen Plattformen einzubetten",
|
||||
"tabs": {
|
||||
"directLink": "Direkter Link",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "Direkte URL zur Bilddatei",
|
||||
"htmlDescription": "Verwenden Sie diesen Code, um das Bild in HTML-Seiten einzubetten",
|
||||
"bbcodeDescription": "Verwenden Sie diesen Code, um das Bild in Foren einzubetten, die BBCode unterstützen"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"rename": "Rename",
|
||||
"move": "Move",
|
||||
"share": "Share",
|
||||
"search": "Search"
|
||||
"search": "Search",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Create Share",
|
||||
@@ -1882,6 +1884,18 @@
|
||||
"userr": "User"
|
||||
}
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Embed Image",
|
||||
"description": "Use these codes to embed this image in forums, websites, or other platforms",
|
||||
"tabs": {
|
||||
"directLink": "Direct Link",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "Direct URL to the image file",
|
||||
"htmlDescription": "Use this code to embed the image in HTML pages",
|
||||
"bbcodeDescription": "Use this code to embed the image in forums that support BBCode"
|
||||
},
|
||||
"validation": {
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
|
@@ -150,7 +150,9 @@
|
||||
"move": "Mover",
|
||||
"rename": "Renombrar",
|
||||
"search": "Buscar",
|
||||
"share": "Compartir"
|
||||
"share": "Compartir",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Crear Compartir",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "Se requiere la contraseña",
|
||||
"nameRequired": "El nombre es obligatorio",
|
||||
"required": "Este campo es obligatorio"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Insertar imagen",
|
||||
"description": "Utiliza estos códigos para insertar esta imagen en foros, sitios web u otras plataformas",
|
||||
"tabs": {
|
||||
"directLink": "Enlace directo",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "URL directa al archivo de imagen",
|
||||
"htmlDescription": "Utiliza este código para insertar la imagen en páginas HTML",
|
||||
"bbcodeDescription": "Utiliza este código para insertar la imagen en foros que admiten BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Déplacer",
|
||||
"rename": "Renommer",
|
||||
"search": "Rechercher",
|
||||
"share": "Partager"
|
||||
"share": "Partager",
|
||||
"copied": "Copié",
|
||||
"copy": "Copier"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Créer un Partage",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "Le mot de passe est requis",
|
||||
"nameRequired": "Nome é obrigatório",
|
||||
"required": "Este campo é obrigatório"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Intégrer l'image",
|
||||
"description": "Utilisez ces codes pour intégrer cette image dans des forums, sites web ou autres plateformes",
|
||||
"tabs": {
|
||||
"directLink": "Lien direct",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "URL directe vers le fichier image",
|
||||
"htmlDescription": "Utilisez ce code pour intégrer l'image dans des pages HTML",
|
||||
"bbcodeDescription": "Utilisez ce code pour intégrer l'image dans des forums prenant en charge BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "स्थानांतरित करें",
|
||||
"rename": "नाम बदलें",
|
||||
"search": "खोजें",
|
||||
"share": "साझा करें"
|
||||
"share": "साझा करें",
|
||||
"copied": "कॉपी किया गया",
|
||||
"copy": "कॉपी करें"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "साझाकरण बनाएं",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "पासवर्ड आवश्यक है",
|
||||
"nameRequired": "नाम आवश्यक है",
|
||||
"required": "यह फ़ील्ड आवश्यक है"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "छवि एम्बेड करें",
|
||||
"description": "इस छवि को मंचों, वेबसाइटों या अन्य प्लेटफार्मों में एम्बेड करने के लिए इन कोड का उपयोग करें",
|
||||
"tabs": {
|
||||
"directLink": "सीधा लिंक",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "छवि फ़ाइल का सीधा URL",
|
||||
"htmlDescription": "HTML पेजों में छवि एम्बेड करने के लिए इस कोड का उपयोग करें",
|
||||
"bbcodeDescription": "BBCode का समर्थन करने वाले मंचों में छवि एम्बेड करने के लिए इस कोड का उपयोग करें"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Sposta",
|
||||
"rename": "Rinomina",
|
||||
"search": "Cerca",
|
||||
"share": "Condividi"
|
||||
"share": "Condividi",
|
||||
"copied": "Copiato",
|
||||
"copy": "Copia"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Crea Condivisione",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordMinLength": "La password deve contenere almeno 6 caratteri",
|
||||
"nameRequired": "Il nome è obbligatorio",
|
||||
"required": "Questo campo è obbligatorio"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Incorpora immagine",
|
||||
"description": "Usa questi codici per incorporare questa immagine in forum, siti web o altre piattaforme",
|
||||
"tabs": {
|
||||
"directLink": "Link diretto",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "URL diretto al file immagine",
|
||||
"htmlDescription": "Usa questo codice per incorporare l'immagine nelle pagine HTML",
|
||||
"bbcodeDescription": "Usa questo codice per incorporare l'immagine nei forum che supportano BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "移動",
|
||||
"rename": "名前を変更",
|
||||
"search": "検索",
|
||||
"share": "共有"
|
||||
"share": "共有",
|
||||
"copied": "コピーしました",
|
||||
"copy": "コピー"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "共有を作成",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "パスワードは必須です",
|
||||
"nameRequired": "名前は必須です",
|
||||
"required": "このフィールドは必須です"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "画像を埋め込む",
|
||||
"description": "これらのコードを使用して、この画像をフォーラム、ウェブサイト、またはその他のプラットフォームに埋め込みます",
|
||||
"tabs": {
|
||||
"directLink": "直接リンク",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "画像ファイルへの直接URL",
|
||||
"htmlDescription": "このコードを使用してHTMLページに画像を埋め込みます",
|
||||
"bbcodeDescription": "BBCodeをサポートするフォーラムに画像を埋め込むには、このコードを使用します"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "이동",
|
||||
"rename": "이름 변경",
|
||||
"search": "검색",
|
||||
"share": "공유"
|
||||
"share": "공유",
|
||||
"copied": "복사됨",
|
||||
"copy": "복사"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "공유 생성",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "비밀번호는 필수입니다",
|
||||
"nameRequired": "이름은 필수입니다",
|
||||
"required": "이 필드는 필수입니다"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "이미지 삽입",
|
||||
"description": "이 코드를 사용하여 포럼, 웹사이트 또는 기타 플랫폼에 이 이미지를 삽입하세요",
|
||||
"tabs": {
|
||||
"directLink": "직접 링크",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "이미지 파일에 대한 직접 URL",
|
||||
"htmlDescription": "이 코드를 사용하여 HTML 페이지에 이미지를 삽입하세요",
|
||||
"bbcodeDescription": "BBCode를 지원하는 포럼에 이미지를 삽입하려면 이 코드를 사용하세요"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Verplaatsen",
|
||||
"rename": "Hernoemen",
|
||||
"search": "Zoeken",
|
||||
"share": "Delen"
|
||||
"share": "Delen",
|
||||
"copied": "Gekopieerd",
|
||||
"copy": "Kopiëren"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Delen Maken",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten",
|
||||
"nameRequired": "Naam is verplicht",
|
||||
"required": "Dit veld is verplicht"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Afbeelding insluiten",
|
||||
"description": "Gebruik deze codes om deze afbeelding in te sluiten in forums, websites of andere platforms",
|
||||
"tabs": {
|
||||
"directLink": "Directe link",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "Directe URL naar het afbeeldingsbestand",
|
||||
"htmlDescription": "Gebruik deze code om de afbeelding in te sluiten in HTML-pagina's",
|
||||
"bbcodeDescription": "Gebruik deze code om de afbeelding in te sluiten in forums die BBCode ondersteunen"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Przenieś",
|
||||
"rename": "Zmień nazwę",
|
||||
"search": "Szukaj",
|
||||
"share": "Udostępnij"
|
||||
"share": "Udostępnij",
|
||||
"copied": "Skopiowano",
|
||||
"copy": "Kopiuj"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Utwórz Udostępnienie",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordMinLength": "Hasło musi mieć co najmniej 6 znaków",
|
||||
"nameRequired": "Nazwa jest wymagana",
|
||||
"required": "To pole jest wymagane"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Osadź obraz",
|
||||
"description": "Użyj tych kodów, aby osadzić ten obraz na forach, stronach internetowych lub innych platformach",
|
||||
"tabs": {
|
||||
"directLink": "Link bezpośredni",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "Bezpośredni adres URL pliku obrazu",
|
||||
"htmlDescription": "Użyj tego kodu, aby osadzić obraz na stronach HTML",
|
||||
"bbcodeDescription": "Użyj tego kodu, aby osadzić obraz na forach obsługujących BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Mover",
|
||||
"rename": "Renomear",
|
||||
"search": "Pesquisar",
|
||||
"share": "Compartilhar"
|
||||
"share": "Compartilhar",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Criar compartilhamento",
|
||||
@@ -1932,5 +1934,17 @@
|
||||
"lastNameRequired": "O sobrenome é necessário",
|
||||
"usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres",
|
||||
"usernameSpaces": "O nome de usuário não pode conter espaços"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Incorporar imagem",
|
||||
"description": "Use estes códigos para incorporar esta imagem em fóruns, sites ou outras plataformas",
|
||||
"tabs": {
|
||||
"directLink": "Link direto",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "URL direto para o arquivo de imagem",
|
||||
"htmlDescription": "Use este código para incorporar a imagem em páginas HTML",
|
||||
"bbcodeDescription": "Use este código para incorporar a imagem em fóruns que suportam BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Переместить",
|
||||
"rename": "Переименовать",
|
||||
"search": "Поиск",
|
||||
"share": "Поделиться"
|
||||
"share": "Поделиться",
|
||||
"copied": "Скопировано",
|
||||
"copy": "Копировать"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Создать общий доступ",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "Требуется пароль",
|
||||
"nameRequired": "Требуется имя",
|
||||
"required": "Это поле обязательно"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Встроить изображение",
|
||||
"description": "Используйте эти коды для встраивания этого изображения на форумах, веб-сайтах или других платформах",
|
||||
"tabs": {
|
||||
"directLink": "Прямая ссылка",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "Прямой URL-адрес файла изображения",
|
||||
"htmlDescription": "Используйте этот код для встраивания изображения в HTML-страницы",
|
||||
"bbcodeDescription": "Используйте этот код для встраивания изображения на форумах, поддерживающих BBCode"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "Taşı",
|
||||
"rename": "Yeniden Adlandır",
|
||||
"search": "Ara",
|
||||
"share": "Paylaş"
|
||||
"share": "Paylaş",
|
||||
"copied": "Kopyalandı",
|
||||
"copy": "Kopyala"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Paylaşım Oluştur",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "Şifre gerekli",
|
||||
"nameRequired": "İsim gereklidir",
|
||||
"required": "Bu alan zorunludur"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "Resmi Yerleştir",
|
||||
"description": "Bu görüntüyü forumlara, web sitelerine veya diğer platformlara yerleştirmek için bu kodları kullanın",
|
||||
"tabs": {
|
||||
"directLink": "Doğrudan Bağlantı",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "Resim dosyasının doğrudan URL'si",
|
||||
"htmlDescription": "Resmi HTML sayfalarına yerleştirmek için bu kodu kullanın",
|
||||
"bbcodeDescription": "BBCode destekleyen forumlara resmi yerleştirmek için bu kodu kullanın"
|
||||
}
|
||||
}
|
@@ -150,7 +150,9 @@
|
||||
"move": "移动",
|
||||
"rename": "重命名",
|
||||
"search": "搜索",
|
||||
"share": "分享"
|
||||
"share": "分享",
|
||||
"copied": "已复制",
|
||||
"copy": "复制"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "创建分享",
|
||||
@@ -1931,5 +1933,17 @@
|
||||
"passwordRequired": "密码为必填项",
|
||||
"nameRequired": "名称为必填项",
|
||||
"required": "此字段为必填项"
|
||||
},
|
||||
"embedCode": {
|
||||
"title": "嵌入图片",
|
||||
"description": "使用这些代码将此图片嵌入到论坛、网站或其他平台中",
|
||||
"tabs": {
|
||||
"directLink": "直接链接",
|
||||
"html": "HTML",
|
||||
"bbcode": "BBCode"
|
||||
},
|
||||
"directLinkDescription": "图片文件的直接URL",
|
||||
"htmlDescription": "使用此代码将图片嵌入HTML页面",
|
||||
"bbcodeDescription": "使用此代码将图片嵌入支持BBCode的论坛"
|
||||
}
|
||||
}
|
71
apps/web/src/app/e/[id]/route.ts
Normal file
71
apps/web/src/app/e/[id]/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333";
|
||||
|
||||
/**
|
||||
* Short public embed endpoint: /e/{id}
|
||||
* No authentication required
|
||||
* Only works for media files (images, videos, audio)
|
||||
*/
|
||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
|
||||
if (!id) {
|
||||
return new NextResponse(JSON.stringify({ error: "File ID is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${API_BASE_URL}/embed/${id}`;
|
||||
|
||||
try {
|
||||
const apiRes = await fetch(url, {
|
||||
method: "GET",
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
if (!apiRes.ok) {
|
||||
const errorText = await apiRes.text();
|
||||
return new NextResponse(errorText, {
|
||||
status: apiRes.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const blob = await apiRes.blob();
|
||||
|
||||
const contentType = apiRes.headers.get("content-type") || "application/octet-stream";
|
||||
const contentDisposition = apiRes.headers.get("content-disposition");
|
||||
const cacheControl = apiRes.headers.get("cache-control");
|
||||
|
||||
const res = new NextResponse(blob, {
|
||||
status: apiRes.status,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
},
|
||||
});
|
||||
|
||||
if (contentDisposition) {
|
||||
res.headers.set("Content-Disposition", contentDisposition);
|
||||
}
|
||||
|
||||
if (cacheControl) {
|
||||
res.headers.set("Cache-Control", cacheControl);
|
||||
} else {
|
||||
res.headers.set("Cache-Control", "public, max-age=31536000");
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error("Error proxying embed request:", error);
|
||||
return new NextResponse(JSON.stringify({ error: "Failed to fetch file" }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
151
apps/web/src/components/files/embed-code-display.tsx
Normal file
151
apps/web/src/components/files/embed-code-display.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface EmbedCodeDisplayProps {
|
||||
imageUrl: string;
|
||||
fileName: string;
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
export function EmbedCodeDisplay({ imageUrl, fileName, fileId }: EmbedCodeDisplayProps) {
|
||||
const t = useTranslations();
|
||||
const [copiedType, setCopiedType] = useState<string | null>(null);
|
||||
const [fullUrl, setFullUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const origin = window.location.origin;
|
||||
const embedUrl = `${origin}/e/${fileId}`;
|
||||
setFullUrl(embedUrl);
|
||||
}
|
||||
}, [fileId]);
|
||||
|
||||
const directLink = fullUrl || imageUrl;
|
||||
const htmlCode = `<img src="${directLink}" alt="${fileName}" />`;
|
||||
const bbCode = `[img]${directLink}[/img]`;
|
||||
|
||||
const copyToClipboard = async (text: string, type: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedType(type);
|
||||
setTimeout(() => setCopiedType(null), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-sm font-semibold">{t("embedCode.title")}</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t("embedCode.description")}</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="direct" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="direct" className="cursor-pointer">
|
||||
{t("embedCode.tabs.directLink")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="html" className="cursor-pointer">
|
||||
{t("embedCode.tabs.html")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bbcode" className="cursor-pointer">
|
||||
{t("embedCode.tabs.bbcode")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="direct" className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={directLink}
|
||||
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted/50 font-mono"
|
||||
/>
|
||||
<Button
|
||||
size="default"
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(directLink, "direct")}
|
||||
className="shrink-0 h-full"
|
||||
>
|
||||
{copiedType === "direct" ? (
|
||||
<>
|
||||
<IconCheck className="h-4 w-4 mr-1" />
|
||||
{t("common.copied")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCopy className="h-4 w-4 mr-1" />
|
||||
{t("common.copy")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("embedCode.directLinkDescription")}</p>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="html" className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={htmlCode}
|
||||
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted/50 font-mono"
|
||||
/>
|
||||
<Button variant="outline" onClick={() => copyToClipboard(htmlCode, "html")} className="shrink-0 h-full">
|
||||
{copiedType === "html" ? (
|
||||
<>
|
||||
<IconCheck className="h-4 w-4 mr-1" />
|
||||
{t("common.copied")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCopy className="h-4 w-4 mr-1" />
|
||||
{t("common.copy")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("embedCode.htmlDescription")}</p>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="bbcode" className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={bbCode}
|
||||
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted/50 font-mono"
|
||||
/>
|
||||
<Button variant="outline" onClick={() => copyToClipboard(bbCode, "bbcode")} className="shrink-0 h-full">
|
||||
{copiedType === "bbcode" ? (
|
||||
<>
|
||||
<IconCheck className="h-4 w-4 mr-1" />
|
||||
{t("common.copied")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCopy className="h-4 w-4 mr-1" />
|
||||
{t("common.copy")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("embedCode.bbcodeDescription")}</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
72
apps/web/src/components/files/media-embed-link.tsx
Normal file
72
apps/web/src/components/files/media-embed-link.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface MediaEmbedLinkProps {
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
export function MediaEmbedLink({ fileId }: MediaEmbedLinkProps) {
|
||||
const t = useTranslations();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [embedUrl, setEmbedUrl] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const origin = window.location.origin;
|
||||
const url = `${origin}/e/${fileId}`;
|
||||
setEmbedUrl(url);
|
||||
}
|
||||
}, [fileId]);
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(embedUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-semibold">{t("embedCode.title")}</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t("embedCode.directLinkDescription")}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={embedUrl}
|
||||
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted/50 font-mono"
|
||||
/>
|
||||
<Button size="default" variant="outline" onClick={copyToClipboard} className="shrink-0 h-full">
|
||||
{copied ? (
|
||||
<>
|
||||
<IconCheck className="h-4 w-4 mr-1" />
|
||||
{t("common.copied")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCopy className="h-4 w-4 mr-1" />
|
||||
{t("common.copy")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -3,6 +3,8 @@
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { EmbedCodeDisplay } from "@/components/files/embed-code-display";
|
||||
import { MediaEmbedLink } from "@/components/files/media-embed-link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { useFilePreview } from "@/hooks/use-file-preview";
|
||||
import { getFileIcon } from "@/utils/file-icons";
|
||||
import { getFileType } from "@/utils/file-types";
|
||||
import { FilePreviewRenderer } from "./previews";
|
||||
|
||||
interface FilePreviewModalProps {
|
||||
@@ -39,6 +42,10 @@ export function FilePreviewModal({
|
||||
}: FilePreviewModalProps) {
|
||||
const t = useTranslations();
|
||||
const previewState = useFilePreview({ file, isOpen, isReverseShare, sharePassword });
|
||||
const fileType = getFileType(file.name);
|
||||
const isImage = fileType === "image";
|
||||
const isVideo = fileType === "video";
|
||||
const isAudio = fileType === "audio";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -67,6 +74,16 @@ export function FilePreviewModal({
|
||||
description={file.description}
|
||||
onDownload={previewState.handleDownload}
|
||||
/>
|
||||
{isImage && previewState.previewUrl && !previewState.isLoading && file.id && (
|
||||
<div className="mt-4 mb-2">
|
||||
<EmbedCodeDisplay imageUrl={previewState.previewUrl} fileName={file.name} fileId={file.id} />
|
||||
</div>
|
||||
)}
|
||||
{(isVideo || isAudio) && !previewState.isLoading && file.id && (
|
||||
<div className="mt-4 mb-2">
|
||||
<MediaEmbedLink fileId={file.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
|
Reference in New Issue
Block a user