gitmaps 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/repo/pdf-meta/route.ts +68 -0
- package/app/lib/cards.tsx +115 -7
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { validateRepoPath } from '../validate-path';
|
|
4
|
+
|
|
5
|
+
function parsePdfInfoPageCount(output: string): number | null {
|
|
6
|
+
const match = output.match(/^Pages:\s+(\d+)/mi);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const value = parseInt(match[1], 10);
|
|
9
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function GET(req: Request) {
|
|
13
|
+
const url = new URL(req.url);
|
|
14
|
+
const repoPath = url.searchParams.get('path') || '';
|
|
15
|
+
const filePath = url.searchParams.get('file') || '';
|
|
16
|
+
|
|
17
|
+
if (!repoPath || !filePath) {
|
|
18
|
+
return Response.json({ error: 'path and file params required' }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const blocked = validateRepoPath(repoPath);
|
|
22
|
+
if (blocked) return blocked;
|
|
23
|
+
|
|
24
|
+
if (filePath.includes('..') || filePath.startsWith('/')) {
|
|
25
|
+
return Response.json({ error: 'Invalid file path' }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fullPath = path.join(repoPath, filePath);
|
|
29
|
+
if (!existsSync(fullPath)) {
|
|
30
|
+
return Response.json({ error: 'File not found' }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const info = Bun.spawnSync(['pdfinfo', fullPath], {
|
|
35
|
+
stdout: 'pipe',
|
|
36
|
+
stderr: 'pipe',
|
|
37
|
+
timeout: 15000,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (info.exitCode === 0) {
|
|
41
|
+
const pageCount = parsePdfInfoPageCount(new TextDecoder().decode(info.stdout));
|
|
42
|
+
return Response.json({ pageCount });
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// pdfinfo unavailable
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const identify = Bun.spawnSync(['magick', 'identify', fullPath], {
|
|
50
|
+
stdout: 'pipe',
|
|
51
|
+
stderr: 'pipe',
|
|
52
|
+
timeout: 20000,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (identify.exitCode === 0) {
|
|
56
|
+
const lines = new TextDecoder().decode(identify.stdout)
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((line) => line.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
const pageCount = lines.length > 0 ? lines.length : null;
|
|
61
|
+
return Response.json({ pageCount });
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// magick unavailable
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Response.json({ pageCount: null });
|
|
68
|
+
}
|
package/app/lib/cards.tsx
CHANGED
|
@@ -965,6 +965,116 @@ export function _buildFileContentHTML(
|
|
|
965
965
|
return `<div class="file-content-preview"><pre><code>${code}</code></pre>${truncNote}</div>`;
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
function buildPdfThumbUrl(repoPath: string, filePath: string, page: number) {
|
|
969
|
+
return `/api/repo/pdf-thumb?path=${encodeURIComponent(repoPath)}&file=${encodeURIComponent(filePath)}&page=${page}`;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function buildPdfPreviewHTML(repoPath: string, file: any) {
|
|
973
|
+
const thumbUrl = buildPdfThumbUrl(repoPath, file.path, 0);
|
|
974
|
+
return `<div class="file-content-preview file-image-preview pdf-preview" data-file="${escapeHtml(file.path)}" data-page="0" data-page-count="" style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--bg-card);overflow:hidden;position:relative;">
|
|
975
|
+
<img class="pdf-preview-image" src="${thumbUrl}"
|
|
976
|
+
alt="${escapeHtml(file.name)}"
|
|
977
|
+
style="max-width:100%;max-height:100%;object-fit:contain;"
|
|
978
|
+
loading="lazy"
|
|
979
|
+
onerror="this.parentElement.innerHTML='<pre><code><span class=\\'error-notice\\'>PDF preview unavailable</span></code></pre>'" />
|
|
980
|
+
<div class="pdf-preview-toolbar" style="position:absolute;left:8px;right:8px;bottom:8px;display:flex;align-items:center;justify-content:center;gap:8px;padding:6px 8px;background:rgba(10,10,15,0.72);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.08);border-radius:10px;z-index:2;">
|
|
981
|
+
<button class="pdf-page-prev" type="button" style="min-width:28px;height:28px;border-radius:7px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.06);color:var(--text-primary);cursor:pointer;" disabled>‹</button>
|
|
982
|
+
<span class="pdf-page-indicator" style="font-size:11px;color:var(--text-primary);min-width:72px;text-align:center;">Page 1</span>
|
|
983
|
+
<button class="pdf-page-next" type="button" style="min-width:28px;height:28px;border-radius:7px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.06);color:var(--text-primary);cursor:pointer;">›</button>
|
|
984
|
+
</div>
|
|
985
|
+
</div>`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function setupPdfPreviewControls(ctx: CanvasContext, card: HTMLElement, file: any) {
|
|
989
|
+
const repoPath = ctx.snap().context.repoPath || "";
|
|
990
|
+
if (!repoPath) return;
|
|
991
|
+
|
|
992
|
+
const preview = card.querySelector('.pdf-preview') as HTMLElement | null;
|
|
993
|
+
const img = card.querySelector('.pdf-preview-image') as HTMLImageElement | null;
|
|
994
|
+
const prevBtn = card.querySelector('.pdf-page-prev') as HTMLButtonElement | null;
|
|
995
|
+
const nextBtn = card.querySelector('.pdf-page-next') as HTMLButtonElement | null;
|
|
996
|
+
const indicator = card.querySelector('.pdf-page-indicator') as HTMLElement | null;
|
|
997
|
+
if (!preview || !img || !prevBtn || !nextBtn || !indicator) return;
|
|
998
|
+
|
|
999
|
+
let page = 0;
|
|
1000
|
+
let pageCount: number | null = null;
|
|
1001
|
+
let lastGoodPage = 0;
|
|
1002
|
+
let loading = false;
|
|
1003
|
+
|
|
1004
|
+
const updateUi = () => {
|
|
1005
|
+
preview.dataset.page = String(page);
|
|
1006
|
+
preview.dataset.pageCount = pageCount == null ? '' : String(pageCount);
|
|
1007
|
+
indicator.textContent = pageCount && pageCount > 0 ? `Page ${page + 1} / ${pageCount}` : `Page ${page + 1}`;
|
|
1008
|
+
prevBtn.disabled = loading || page <= 0;
|
|
1009
|
+
nextBtn.disabled = loading || (pageCount != null && page >= pageCount - 1);
|
|
1010
|
+
prevBtn.style.opacity = prevBtn.disabled ? '0.45' : '1';
|
|
1011
|
+
nextBtn.style.opacity = nextBtn.disabled ? '0.45' : '1';
|
|
1012
|
+
prevBtn.style.cursor = prevBtn.disabled ? 'default' : 'pointer';
|
|
1013
|
+
nextBtn.style.cursor = nextBtn.disabled ? 'default' : 'pointer';
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const loadPage = (nextPage: number) => {
|
|
1017
|
+
if (loading || nextPage < 0) return;
|
|
1018
|
+
if (pageCount != null && nextPage >= pageCount) return;
|
|
1019
|
+
|
|
1020
|
+
loading = true;
|
|
1021
|
+
page = nextPage;
|
|
1022
|
+
updateUi();
|
|
1023
|
+
|
|
1024
|
+
const nextSrc = buildPdfThumbUrl(repoPath, file.path, nextPage);
|
|
1025
|
+
const rollbackPage = lastGoodPage;
|
|
1026
|
+
|
|
1027
|
+
img.onload = () => {
|
|
1028
|
+
lastGoodPage = nextPage;
|
|
1029
|
+
loading = false;
|
|
1030
|
+
img.onload = null;
|
|
1031
|
+
img.onerror = null;
|
|
1032
|
+
updateUi();
|
|
1033
|
+
};
|
|
1034
|
+
img.onerror = () => {
|
|
1035
|
+
loading = false;
|
|
1036
|
+
if (pageCount == null && nextPage > rollbackPage) {
|
|
1037
|
+
pageCount = nextPage;
|
|
1038
|
+
}
|
|
1039
|
+
page = rollbackPage;
|
|
1040
|
+
img.onload = () => {
|
|
1041
|
+
lastGoodPage = rollbackPage;
|
|
1042
|
+
img.onload = null;
|
|
1043
|
+
img.onerror = null;
|
|
1044
|
+
updateUi();
|
|
1045
|
+
};
|
|
1046
|
+
img.onerror = null;
|
|
1047
|
+
img.src = buildPdfThumbUrl(repoPath, file.path, rollbackPage);
|
|
1048
|
+
updateUi();
|
|
1049
|
+
};
|
|
1050
|
+
img.src = nextSrc;
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
prevBtn.addEventListener('click', (e) => {
|
|
1054
|
+
e.stopPropagation();
|
|
1055
|
+
loadPage(page - 1);
|
|
1056
|
+
});
|
|
1057
|
+
nextBtn.addEventListener('click', (e) => {
|
|
1058
|
+
e.stopPropagation();
|
|
1059
|
+
loadPage(page + 1);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
updateUi();
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
1065
|
+
const res = await fetch(`/api/repo/pdf-meta?path=${encodeURIComponent(repoPath)}&file=${encodeURIComponent(file.path)}`);
|
|
1066
|
+
if (res.ok) {
|
|
1067
|
+
const data = await res.json();
|
|
1068
|
+
if (Number.isFinite(data?.pageCount) && data.pageCount > 0) {
|
|
1069
|
+
pageCount = data.pageCount;
|
|
1070
|
+
updateUi();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
// Metadata is optional — controls still work with optimistic next/prev.
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
968
1078
|
// ─── Create all-file card (working tree) ────────────────
|
|
969
1079
|
export function createAllFileCard(
|
|
970
1080
|
ctx: CanvasContext,
|
|
@@ -1026,13 +1136,7 @@ export function createAllFileCard(
|
|
|
1026
1136
|
loading="lazy" />
|
|
1027
1137
|
</div>`;
|
|
1028
1138
|
} else if (isPdf) {
|
|
1029
|
-
contentHTML =
|
|
1030
|
-
<img src="/api/repo/pdf-thumb?path=${encodeURIComponent(ctx.snap().context.repoPath || "")}&file=${encodeURIComponent(file.path)}"
|
|
1031
|
-
alt="${escapeHtml(file.name)}"
|
|
1032
|
-
style="max-width:100%;max-height:100%;object-fit:contain;"
|
|
1033
|
-
loading="lazy"
|
|
1034
|
-
onerror="this.parentElement.innerHTML='<pre><code><span class=\\'error-notice\\'>PDF preview unavailable</span></code></pre>'" />
|
|
1035
|
-
</div>`;
|
|
1139
|
+
contentHTML = buildPdfPreviewHTML(ctx.snap().context.repoPath || "", file);
|
|
1036
1140
|
} else if (file.isBinary) {
|
|
1037
1141
|
contentHTML = `<div class="file-content-preview"><pre><code><span class="error-notice">Binary file</span></code></pre></div>`;
|
|
1038
1142
|
} else if (file.content) {
|
|
@@ -1208,6 +1312,10 @@ export function createAllFileCard(
|
|
|
1208
1312
|
});
|
|
1209
1313
|
}
|
|
1210
1314
|
|
|
1315
|
+
if (isPdf) {
|
|
1316
|
+
setupPdfPreviewControls(ctx, card, file);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1211
1319
|
if (canvasOptions) {
|
|
1212
1320
|
const previewEl = card.querySelector(".canvas-container") as HTMLElement;
|
|
1213
1321
|
if (previewEl) {
|