Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add PDF screenshot generation and display #995

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9734ea1
Updated pdf2json to 3.1.5
AhmadMuj Feb 5, 2025
01173c4
Extract and store a screenshot from PDF files using pdf2pic
AhmadMuj Feb 8, 2025
c9b3892
Installing graphicsmagick and ghostscript
AhmadMuj Feb 8, 2025
403f873
Generate Missing PDF screenshot with tidyAssets worker for backward s…
AhmadMuj Feb 8, 2025
b370b57
Display PDF screenshot instead of the PDF in web if it exists.
AhmadMuj Feb 8, 2025
4dae477
Display PDF screenshot in mobile app if exists.
AhmadMuj Feb 8, 2025
b3c857a
Updated pnpm-lock.yaml
AhmadMuj Feb 8, 2025
5caa9a1
Removed console.log
AhmadMuj Feb 9, 2025
6d59073
Revert the unnecessary changes in package.json
AhmadMuj Feb 9, 2025
050778e
Revert pnpm-lock changes
AhmadMuj Feb 9, 2025
57c3072
Prevent rendering PDF files if the screenshot is not generated
AhmadMuj Feb 9, 2025
235de11
refactor: replace useEffect with useMemo for section initialization
AhmadMuj Feb 15, 2025
f2aef9d
feat: show PDF file download button and handle large PDFs by defaulti…
AhmadMuj Feb 15, 2025
bdb4863
feat: add file size to openapi spec
AhmadMuj Feb 15, 2025
0884764
feature: Add Assets preprocessing in fix mode to admin actions
AhmadMuj Feb 15, 2025
fbfc823
i18n: add reprocess_assets_fix_mode translation
AhmadMuj Feb 15, 2025
b6bd4f7
Merge branch 'main' into feat/pdf-thumbnail-preprocessing
AhmadMuj Feb 15, 2025
744ef20
i18n: Add missing ar translations
AhmadMuj Feb 15, 2025
34888d5
A bunch of fixes
MohamedBassem Feb 16, 2025
5eade27
Fix openspec schema
MohamedBassem Feb 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/mobile/components/bookmarks/BookmarkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import {
ActivityIndicator,
Alert,
Expand Down Expand Up @@ -300,11 +301,15 @@ function AssetCard({
}
const title = bookmark.title ?? bookmark.content.fileName;

const assetImage =
bookmark.assets.find((r) => r.assetType == "assetScreenshot")?.id ??
bookmark.content.assetId;

return (
<View className="flex gap-2">
<Pressable onPress={onOpenBookmark}>
<BookmarkAssetImage
assetId={bookmark.content.assetId}
assetId={assetImage}
className="h-56 min-h-56 w-full object-cover"
/>
</Pressable>
Expand Down
22 changes: 22 additions & 0 deletions apps/web/components/admin/AdminActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ export default function AdminActions() {
},
});

const { mutate: reprocessAssetsFixMode, isPending: isReprocessingPending } =
api.admin.reprocessAssetsFixMode.useMutation({
onSuccess: () => {
toast({
description: "Reprocessing enqueued",
});
},
onError: (e) => {
toast({
variant: "destructive",
description: e.message,
});
},
});

const {
mutate: reRunInferenceOnAllBookmarks,
isPending: isInferencePending,
Expand Down Expand Up @@ -124,6 +139,13 @@ export default function AdminActions() {
>
{t("admin.actions.reindex_all_bookmarks")}
</ActionButton>
<ActionButton
variant="destructive"
loading={isReprocessingPending}
onClick={() => reprocessAssetsFixMode()}
>
{t("admin.actions.reprocess_assets_fix_mode")}
</ActionButton>
<ActionButton
variant="destructive"
loading={isTidyAssetsPending}
Expand Down
28 changes: 23 additions & 5 deletions apps/web/components/dashboard/bookmarks/AssetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import Image from "next/image";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { FileText } from "lucide-react";

import type { ZBookmarkTypeAsset } from "@hoarder/shared/types/bookmarks";
import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils";
Expand Down Expand Up @@ -32,12 +34,28 @@ function AssetImage({
);
}
case "pdf": {
const screenshotAssetId = bookmark.assets.find(
(r) => r.assetType === "assetScreenshot",
)?.id;
if (!screenshotAssetId) {
return (
<div
className={cn(className, "flex items-center justify-center")}
title="PDF screenshot not available. Run asset preprocessing job to generate one screenshot"
>
<FileText size={80} />
</div>
);
}
return (
<iframe
title={bookmarkedAsset.assetId}
className={className}
src={getAssetUrl(bookmarkedAsset.assetId)}
/>
<Link href={`/dashboard/preview/${bookmark.id}`}>
<Image
alt="asset"
src={getAssetUrl(screenshotAssetId)}
fill={true}
className={className}
/>
</Link>
);
}
default: {
Expand Down
131 changes: 103 additions & 28 deletions apps/web/components/dashboard/preview/AssetContentSection.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,117 @@
import { useMemo, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useTranslation } from "@/lib/i18n/client";

import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils";
import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";

export function AssetContentSection({ bookmark }: { bookmark: ZBookmark }) {
// 20 MB
const BIG_FILE_SIZE = 20 * 1024 * 1024;

function PDFContentSection({ bookmark }: { bookmark: ZBookmark }) {
if (bookmark.content.type != BookmarkTypes.ASSET) {
throw new Error("Invalid content type");
}
const { t } = useTranslation();

switch (bookmark.content.assetType) {
case "image": {
return (
<div className="relative h-full min-w-full">
<Link
href={`/api/assets/${bookmark.content.assetId}`}
target="_blank"
>
<Image
alt="asset"
fill={true}
className="object-contain"
src={`/api/assets/${bookmark.content.assetId}`}
/>
</Link>
</div>
);
const initialSection = useMemo(() => {
if (bookmark.content.type != BookmarkTypes.ASSET) {
throw new Error("Invalid content type");
}
case "pdf": {
return (
<iframe
title={bookmark.content.assetId}
className="h-full w-full"
src={`/api/assets/${bookmark.content.assetId}`}
/>
);

const screenshot = bookmark.assets.find(
(item) => item.assetType === "assetScreenshot",
);
const bigSize =
bookmark.content.size && bookmark.content.size > BIG_FILE_SIZE;
if (bigSize && screenshot) {
return "screenshot";
}
default: {
return "pdf";
}, [bookmark]);
const [section, setSection] = useState(initialSection);

const screenshot = bookmark.assets.find(
(r) => r.assetType === "assetScreenshot",
)?.id;

const content =
section === "screenshot" && screenshot ? (
<div className="relative h-full min-w-full">
<Image
alt="screenshot"
src={getAssetUrl(screenshot)}
fill={true}
className="object-contain"
/>
</div>
) : (
<iframe
title={bookmark.content.assetId}
className="h-full w-full"
src={getAssetUrl(bookmark.content.assetId)}
/>
);

return (
<div className="flex h-full flex-col items-center gap-2">
<div className="flex w-full items-center justify-center gap-4">
<Select onValueChange={setSection} value={section}>
<SelectTrigger className="w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="screenshot" disabled={!screenshot}>
{t("common.screenshot")}
</SelectItem>
<SelectItem value="pdf">PDF</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
{content}
</div>
);
}

function ImageContentSection({ bookmark }: { bookmark: ZBookmark }) {
if (bookmark.content.type != BookmarkTypes.ASSET) {
throw new Error("Invalid content type");
}
return (
<div className="relative h-full min-w-full">
<Link href={getAssetUrl(bookmark.content.assetId)} target="_blank">
<Image
alt="asset"
fill={true}
className="object-contain"
src={getAssetUrl(bookmark.content.assetId)}
/>
</Link>
</div>
);
}

export function AssetContentSection({ bookmark }: { bookmark: ZBookmark }) {
if (bookmark.content.type != BookmarkTypes.ASSET) {
throw new Error("Invalid content type");
}
switch (bookmark.content.assetType) {
case "image":
return <ImageContentSection bookmark={bookmark} />;
case "pdf":
return <PDFContentSection bookmark={bookmark} />;
default:
return <div>Unsupported asset type</div>;
}
}
}
1 change: 1 addition & 0 deletions apps/web/components/dashboard/preview/AttachmentBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
const { t } = useTranslation();
const typeToIcon: Record<ZAssetType, React.ReactNode> = {
screenshot: <Camera className="size-4" />,
assetScreenshot: <Camera className="size-4" />,
fullPageArchive: <Archive className="size-4" />,
precrawledArchive: <Archive className="size-4" />,
bannerImage: <Image className="size-4" />,
Expand Down
33 changes: 27 additions & 6 deletions apps/web/lib/i18n/locales/ar/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,44 @@
}
},
"admin": {
"admin_settings": "إعدادات المدير",
"admin_settings": "إعدادات المشرف",
"server_stats": {
"server_stats": "إحصائيات الخادم",
"total_users": "إجمالي المستخدمين",
"total_bookmarks": "إجمالي الإشارات المرجعية",
"server_version": "إصدار الخادم"
},
"background_jobs": {
"background_jobs": "المهام التلقائية",
"background_jobs": "المهام الخلفية",
"crawler_jobs": "مهام الاستكشاف",
"indexing_jobs": "مهام الفهرسة",
"inference_jobs": "مهام التحليل الذكي",
"tidy_assets_jobs": "مهام تنظيم الملفات",
"inference_jobs": "مهام الاستدلال",
"tidy_assets_jobs": "مهام تنظيم الوسائط",
"job": "مهمة",
"queued": "في قائمة الانتظار",
"pending": "معلق",
"failed": "فشل"
"pending": "قيد الانتظار",
"failed": "فشلت"
},
"actions": {
"recrawl_failed_links_only": "إعادة استكشاف الروابط الفاشلة فقط",
"recrawl_all_links": "إعادة استكشاف جميع الروابط",
"without_inference": "بدون استدلال",
"regenerate_ai_tags_for_failed_bookmarks_only": "إعادة إنشاء علامات الذكاء الاصطناعي للإشارات المرجعية الفاشلة فقط",
"regenerate_ai_tags_for_all_bookmarks": "إعادة إنشاء علامات الذكاء الاصطناعي لجميع الإشارات المرجعية",
"reindex_all_bookmarks": "إعادة فهرسة جميع الإشارات المرجعية",
"compact_assets": "ضغط الوسائط",
"reprocess_assets_fix_mode": "إعادة معالجة الوسائط (وضع الإصلاح)"
},
"users_list": {
"users_list": "قائمة المستخدمين",
"create_user": "إنشاء مستخدم",
"change_role": "تغيير الدور",
"reset_password": "إعادة تعيين كلمة المرور",
"delete_user": "حذف المستخدم",
"num_bookmarks": "عدد الإشارات المرجعية",
"asset_sizes": "أحجام الوسائط",
"local_user": "مستخدم محلي",
"confirm_password": "تأكيد كلمة المرور"
}
},
"options": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/da/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@
"recrawl_all_links": "Gennemsøg alle links",
"without_inference": "Uden inferens",
"regenerate_ai_tags_for_all_bookmarks": "Genopret AI-tags for alle bogmærker",
"reindex_all_bookmarks": "Genindeksér alle bogmærker"
"reindex_all_bookmarks": "Genindeksér alle bogmærker",
"reprocess_assets_fix_mode": "Genbehandling af aktiver (Fix Mode)"
},
"background_jobs": {
"inference_jobs": "Inferensopgaver",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@
"regenerate_ai_tags_for_failed_bookmarks_only": "KI-Tags nur für fehlgeschlagene Lesezeichen neu generieren",
"regenerate_ai_tags_for_all_bookmarks": "KI-Tags für alle Lesezeichen neu generieren",
"reindex_all_bookmarks": "Alle Lesezeichen neu indizieren",
"compact_assets": "Assets komprimieren"
"compact_assets": "Assets komprimieren",
"reprocess_assets_fix_mode": "Assets neu verarbeiten (Fix-Modus)"
},
"users_list": {
"users_list": "Benutzerliste",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@
"regenerate_ai_tags_for_failed_bookmarks_only": "Regenerate AI Tags for Failed Bookmarks Only",
"regenerate_ai_tags_for_all_bookmarks": "Regenerate AI Tags for All Bookmarks",
"reindex_all_bookmarks": "Reindex All Bookmarks",
"compact_assets": "Compact Assets"
"compact_assets": "Compact Assets",
"reprocess_assets_fix_mode": "Reprocess Assets (Fix Mode)"
},
"users_list": {
"users_list": "Users List",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@
"compact_assets": "Optimizar multimedia",
"without_inference": "Sin inferencia",
"recrawl_failed_links_only": "Recrawlear solo los enlaces fallidos",
"recrawl_all_links": "Recrawlear todos los enlaces"
"recrawl_all_links": "Recrawlear todos los enlaces",
"reprocess_assets_fix_mode": "Reprocesar assets (modo fijo)"
},
"users_list": {
"users_list": "Lista de usuarios",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@
"regenerate_ai_tags_for_failed_bookmarks_only": "Régénérer les tags AI uniquement pour les favoris échoués",
"regenerate_ai_tags_for_all_bookmarks": "Régénérer les tags AI pour tous les favoris",
"reindex_all_bookmarks": "Réindexer tous les favoris",
"compact_assets": "Compacter les assets"
"compact_assets": "Compacter les assets",
"reprocess_assets_fix_mode": "Reprocesser les assets (mode fix)"
},
"users_list": {
"users_list": "Liste des utilisateurs",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/gl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@
"regenerate_ai_tags_for_failed_bookmarks_only": "Rexenerar etiquetas IA so en marcadores errados",
"regenerate_ai_tags_for_all_bookmarks": "Rexenerar etiquetas IA para todos os marcadores",
"reindex_all_bookmarks": "Reindexar marcadores",
"compact_assets": "Optimizar multimedia"
"compact_assets": "Optimizar multimedia",
"reprocess_assets_fix_mode": "Reprocesar assets (modo fixo)"
},
"users_list": {
"users_list": "Listado de usuarios",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/hr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"recrawl_all_links": "Ponovno pregledavanje svih veza",
"regenerate_ai_tags_for_all_bookmarks": "Ponovno generiranje AI oznaka za sve oznake",
"without_inference": "Bez zaključivanja",
"compact_assets": "Kompaktiranje resursa"
"compact_assets": "Kompaktiranje resursa",
"reprocess_assets_fix_mode": "Ponovno postupanje s resursima (fiksni mod)"
}
},
"layouts": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/hu/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@
"regenerate_ai_tags_for_all_bookmarks": "Minden könyvjelző MI címkéjének lecserélése",
"regenerate_ai_tags_for_failed_bookmarks_only": "Hibás könyvjelzők MI címkéjének lecserélése",
"reindex_all_bookmarks": "Minden könyvjelző újraindexelése",
"compact_assets": "Kompakt tulajdonok"
"compact_assets": "Kompakt tulajdonok",
"reprocess_assets_fix_mode": "Tulajdonok függvényezése (Fix Mod)"
},
"users_list": {
"asset_sizes": "Tulajdon méretek",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/i18n/locales/it/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@
"regenerate_ai_tags_for_failed_bookmarks_only": "Rigenera tag AI solo per i segnalibri falliti",
"regenerate_ai_tags_for_all_bookmarks": "Rigenera tag AI per tutti i segnalibri",
"compact_assets": "Compatta asset",
"reindex_all_bookmarks": "Reindicizza tutti i segnalibri"
"reindex_all_bookmarks": "Reindicizza tutti i segnalibri",
"reprocess_assets_fix_mode": "Riprocessa asset (modalità fissa)"
},
"users_list": {
"users_list": "Lista utenti",
Expand Down
Loading