gitmaps 1.1.2 → 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/analytics.db CHANGED
Binary file
@@ -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 = `<div class="file-content-preview file-image-preview" style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--bg-card);overflow:hidden;">
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmaps",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gitmaps": "cli.ts"
package/server.ts CHANGED
@@ -69,4 +69,5 @@ const websocket = {
69
69
  serve(createAppRouter({
70
70
  appDir,
71
71
  globalCss: path.join(appDir, 'globals.css'),
72
- }), { port: parseInt(process.env.PORT || process.env.BUN_PORT || "3335"), websocket });
72
+ hotReload: false,
73
+ }), { port: parseInt(process.env.PORT || process.env.BUN_PORT || "3335"), websocket, hotReload: false });