simple-photo-gallery 2.0.9 → 2.0.10-rc.1

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/dist/index.js CHANGED
@@ -3,74 +3,19 @@ import process3, { stdout } from 'process';
3
3
  import { Command } from 'commander';
4
4
  import { LogLevels, createConsola } from 'consola';
5
5
  import { execSync, spawn } from 'child_process';
6
- import fs6, { promises } from 'fs';
7
- import path4 from 'path';
8
- import { z } from 'zod';
6
+ import fs8, { promises } from 'fs';
7
+ import path7 from 'path';
9
8
  import { Buffer } from 'buffer';
10
9
  import sharp2 from 'sharp';
11
- import ExifReader from 'exifreader';
12
10
  import { encode } from 'blurhash';
11
+ import ExifReader from 'exifreader';
12
+ import { z } from 'zod';
13
13
  import ffprobe from 'node-ffprobe';
14
14
  import os from 'os';
15
15
  import Conf from 'conf';
16
16
  import axios from 'axios';
17
17
  import { compareSemVer, parseSemVer } from 'semver-parser';
18
18
 
19
- var ThumbnailSchema = z.object({
20
- path: z.string(),
21
- pathRetina: z.string(),
22
- width: z.number(),
23
- height: z.number(),
24
- blurHash: z.string().optional()
25
- });
26
- var MediaFileSchema = z.object({
27
- type: z.enum(["image", "video"]),
28
- path: z.string(),
29
- alt: z.string().optional(),
30
- width: z.number(),
31
- height: z.number(),
32
- thumbnail: ThumbnailSchema.optional(),
33
- lastMediaTimestamp: z.string().optional()
34
- });
35
- var GallerySectionSchema = z.object({
36
- title: z.string().optional(),
37
- description: z.string().optional(),
38
- images: z.array(MediaFileSchema)
39
- });
40
- var SubGallerySchema = z.object({
41
- title: z.string(),
42
- headerImage: z.string(),
43
- path: z.string()
44
- });
45
- var GalleryMetadataSchema = z.object({
46
- image: z.string().optional(),
47
- imageWidth: z.number().optional(),
48
- imageHeight: z.number().optional(),
49
- ogUrl: z.string().optional(),
50
- ogType: z.string().optional(),
51
- ogSiteName: z.string().optional(),
52
- twitterSite: z.string().optional(),
53
- twitterCreator: z.string().optional(),
54
- author: z.string().optional(),
55
- keywords: z.string().optional(),
56
- canonicalUrl: z.string().optional(),
57
- language: z.string().optional(),
58
- robots: z.string().optional()
59
- });
60
- var GalleryDataSchema = z.object({
61
- title: z.string(),
62
- description: z.string(),
63
- url: z.string().optional(),
64
- headerImage: z.string(),
65
- thumbnailSize: z.number().optional(),
66
- metadata: GalleryMetadataSchema,
67
- galleryOutputPath: z.string().optional(),
68
- mediaBaseUrl: z.string().optional(),
69
- analyticsScript: z.string().optional(),
70
- sections: z.array(GallerySectionSchema),
71
- subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
72
- });
73
-
74
19
  // src/config/index.ts
75
20
  var DEFAULT_THUMBNAIL_SIZE = 300;
76
21
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".tif", ".svg", ".avif"]);
@@ -138,13 +83,22 @@ async function createImageThumbnails(image, metadata, outputPath, outputPathReti
138
83
  return { width, height };
139
84
  }
140
85
 
86
+ // src/utils/blurhash.ts
87
+ async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
88
+ const image = await loadImage(imagePath);
89
+ const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
90
+ const pixels = new Uint8ClampedArray(data.buffer);
91
+ return encode(pixels, info.width, info.height, componentX, componentY);
92
+ }
93
+
141
94
  // src/modules/build/utils/index.ts
142
- path4.dirname(new URL(import.meta.url).pathname);
95
+ path7.dirname(new URL(import.meta.url).pathname);
143
96
  async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPath, ui) {
144
97
  ui.start(`Creating social media card image`);
145
- if (fs6.existsSync(ouputPath)) {
98
+ const headerBasename = path7.basename(headerPhotoPath, path7.extname(headerPhotoPath));
99
+ if (fs8.existsSync(ouputPath)) {
146
100
  ui.success(`Social media card image already exists`);
147
- return;
101
+ return headerBasename;
148
102
  }
149
103
  const image = await loadImage(headerPhotoPath);
150
104
  const resizedImageBuffer = await image.resize(1200, 631, { fit: "cover" }).jpeg({ quality: 90 }).toBuffer();
@@ -164,76 +118,115 @@ async function createGallerySocialMediaCardImage(headerPhotoPath, title, ouputPa
164
118
  const finalImageBuffer = await sharp2(resizedImageBuffer).composite([{ input: Buffer.from(svgText), top: 0, left: 0 }]).jpeg({ quality: 90 }).toBuffer();
165
119
  await sharp2(finalImageBuffer).toFile(outputPath);
166
120
  ui.success(`Created social media card image successfully`);
121
+ return headerBasename;
167
122
  }
168
123
  async function createOptimizedHeaderImage(headerPhotoPath, outputFolder, ui) {
169
124
  ui.start(`Creating optimized header images`);
170
125
  const image = await loadImage(headerPhotoPath);
126
+ const headerBasename = path7.basename(headerPhotoPath, path7.extname(headerPhotoPath));
127
+ const generatedFiles = [];
128
+ ui.debug("Generating blurhash for header image");
129
+ const blurHash = await generateBlurHash(headerPhotoPath);
171
130
  const landscapeYFactor = 3 / 4;
172
131
  for (const width of HEADER_IMAGE_LANDSCAPE_WIDTHS) {
173
132
  ui.debug(`Creating landscape header image ${width}`);
174
- if (fs6.existsSync(path4.join(outputFolder, `header_landscape_${width}.avif`))) {
133
+ const avifFilename = `${headerBasename}_landscape_${width}.avif`;
134
+ const jpgFilename = `${headerBasename}_landscape_${width}.jpg`;
135
+ if (fs8.existsSync(path7.join(outputFolder, avifFilename))) {
175
136
  ui.debug(`Landscape header image ${width} AVIF already exists`);
176
137
  } else {
177
138
  await cropAndResizeImage(
178
139
  image.clone(),
179
- path4.join(outputFolder, `header_landscape_${width}.avif`),
140
+ path7.join(outputFolder, avifFilename),
180
141
  width,
181
142
  width * landscapeYFactor,
182
143
  "avif"
183
144
  );
184
145
  }
185
- if (fs6.existsSync(path4.join(outputFolder, `header_landscape_${width}.jpg`))) {
146
+ generatedFiles.push(avifFilename);
147
+ if (fs8.existsSync(path7.join(outputFolder, jpgFilename))) {
186
148
  ui.debug(`Landscape header image ${width} JPG already exists`);
187
149
  } else {
188
- await cropAndResizeImage(
189
- image.clone(),
190
- path4.join(outputFolder, `header_landscape_${width}.jpg`),
191
- width,
192
- width * landscapeYFactor,
193
- "jpg"
194
- );
150
+ await cropAndResizeImage(image.clone(), path7.join(outputFolder, jpgFilename), width, width * landscapeYFactor, "jpg");
195
151
  }
152
+ generatedFiles.push(jpgFilename);
196
153
  }
197
154
  const portraitYFactor = 4 / 3;
198
155
  for (const width of HEADER_IMAGE_PORTRAIT_WIDTHS) {
199
156
  ui.debug(`Creating portrait header image ${width}`);
200
- if (fs6.existsSync(path4.join(outputFolder, `header_portrait_${width}.avif`))) {
157
+ const avifFilename = `${headerBasename}_portrait_${width}.avif`;
158
+ const jpgFilename = `${headerBasename}_portrait_${width}.jpg`;
159
+ if (fs8.existsSync(path7.join(outputFolder, avifFilename))) {
201
160
  ui.debug(`Portrait header image ${width} AVIF already exists`);
202
161
  } else {
203
- await cropAndResizeImage(
204
- image.clone(),
205
- path4.join(outputFolder, `header_portrait_${width}.avif`),
206
- width,
207
- width * portraitYFactor,
208
- "avif"
209
- );
162
+ await cropAndResizeImage(image.clone(), path7.join(outputFolder, avifFilename), width, width * portraitYFactor, "avif");
210
163
  }
211
- if (fs6.existsSync(path4.join(outputFolder, `header_portrait_${width}.jpg`))) {
164
+ generatedFiles.push(avifFilename);
165
+ if (fs8.existsSync(path7.join(outputFolder, jpgFilename))) {
212
166
  ui.debug(`Portrait header image ${width} JPG already exists`);
213
167
  } else {
214
- await cropAndResizeImage(
215
- image.clone(),
216
- path4.join(outputFolder, `header_portrait_${width}.jpg`),
217
- width,
218
- width * portraitYFactor,
219
- "jpg"
220
- );
168
+ await cropAndResizeImage(image.clone(), path7.join(outputFolder, jpgFilename), width, width * portraitYFactor, "jpg");
221
169
  }
170
+ generatedFiles.push(jpgFilename);
222
171
  }
223
172
  ui.success(`Created optimized header image successfully`);
173
+ return { headerBasename, generatedFiles, blurHash };
174
+ }
175
+ function hasOldHeaderImages(outputFolder, currentHeaderBasename) {
176
+ if (!fs8.existsSync(outputFolder)) {
177
+ return false;
178
+ }
179
+ const files = fs8.readdirSync(outputFolder);
180
+ for (const file of files) {
181
+ const landscapeMatch = file.match(/^(.+)_landscape_\d+\.(avif|jpg)$/);
182
+ const portraitMatch = file.match(/^(.+)_portrait_\d+\.(avif|jpg)$/);
183
+ if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename || portraitMatch && portraitMatch[1] !== currentHeaderBasename) {
184
+ return true;
185
+ }
186
+ }
187
+ return false;
188
+ }
189
+ function cleanupOldHeaderImages(outputFolder, currentHeaderBasename, ui) {
190
+ ui.start(`Cleaning up old header images`);
191
+ if (!fs8.existsSync(outputFolder)) {
192
+ ui.debug(`Output folder ${outputFolder} does not exist, skipping cleanup`);
193
+ return;
194
+ }
195
+ const files = fs8.readdirSync(outputFolder);
196
+ let deletedCount = 0;
197
+ for (const file of files) {
198
+ const landscapeMatch = file.match(/^(.+)_landscape_\d+\.(avif|jpg)$/);
199
+ const portraitMatch = file.match(/^(.+)_portrait_\d+\.(avif|jpg)$/);
200
+ if (landscapeMatch && landscapeMatch[1] !== currentHeaderBasename) {
201
+ const filePath = path7.join(outputFolder, file);
202
+ ui.debug(`Deleting old landscape header image: ${file}`);
203
+ fs8.unlinkSync(filePath);
204
+ deletedCount++;
205
+ } else if (portraitMatch && portraitMatch[1] !== currentHeaderBasename) {
206
+ const filePath = path7.join(outputFolder, file);
207
+ ui.debug(`Deleting old portrait header image: ${file}`);
208
+ fs8.unlinkSync(filePath);
209
+ deletedCount++;
210
+ }
211
+ }
212
+ if (deletedCount > 0) {
213
+ ui.success(`Deleted ${deletedCount} old header image(s)`);
214
+ } else {
215
+ ui.debug(`No old header images to clean up`);
216
+ }
224
217
  }
225
218
  function findGalleries(basePath, recursive) {
226
219
  const galleryDirs = [];
227
- const galleryJsonPath = path4.join(basePath, "gallery", "gallery.json");
228
- if (fs6.existsSync(galleryJsonPath)) {
220
+ const galleryJsonPath = path7.join(basePath, "gallery", "gallery.json");
221
+ if (fs8.existsSync(galleryJsonPath)) {
229
222
  galleryDirs.push(basePath);
230
223
  }
231
224
  if (recursive) {
232
225
  try {
233
- const entries = fs6.readdirSync(basePath, { withFileTypes: true });
226
+ const entries = fs8.readdirSync(basePath, { withFileTypes: true });
234
227
  for (const entry of entries) {
235
228
  if (entry.isDirectory() && entry.name !== "gallery") {
236
- const subPath = path4.join(basePath, entry.name);
229
+ const subPath = path7.join(basePath, entry.name);
237
230
  const subResults = findGalleries(subPath, recursive);
238
231
  galleryDirs.push(...subResults);
239
232
  }
@@ -261,16 +254,335 @@ function parseTelemetryOption(value) {
261
254
  }
262
255
  return value;
263
256
  }
257
+ var ThumbnailSchema = z.object({
258
+ path: z.string(),
259
+ pathRetina: z.string(),
260
+ width: z.number(),
261
+ height: z.number(),
262
+ blurHash: z.string().optional()
263
+ });
264
+ var MediaFileSchema = z.object({
265
+ type: z.enum(["image", "video"]),
266
+ filename: z.string(),
267
+ alt: z.string().optional(),
268
+ width: z.number(),
269
+ height: z.number(),
270
+ thumbnail: ThumbnailSchema.optional(),
271
+ lastMediaTimestamp: z.string().optional()
272
+ });
273
+ var MediaFileDeprecatedSchema = z.object({
274
+ type: z.enum(["image", "video"]),
275
+ path: z.string(),
276
+ alt: z.string().optional(),
277
+ width: z.number(),
278
+ height: z.number(),
279
+ thumbnail: ThumbnailSchema.optional(),
280
+ lastMediaTimestamp: z.string().optional()
281
+ });
282
+ var GallerySectionSchema = z.object({
283
+ title: z.string().optional(),
284
+ description: z.string().optional(),
285
+ images: z.array(MediaFileSchema)
286
+ });
287
+ var GallerySectionDeprecatedSchema = z.object({
288
+ title: z.string().optional(),
289
+ description: z.string().optional(),
290
+ images: z.array(MediaFileDeprecatedSchema)
291
+ });
292
+ var SubGallerySchema = z.object({
293
+ title: z.string(),
294
+ headerImage: z.string(),
295
+ path: z.string()
296
+ });
297
+ var GalleryMetadataSchema = z.object({
298
+ image: z.string().optional(),
299
+ imageWidth: z.number().optional(),
300
+ imageHeight: z.number().optional(),
301
+ ogUrl: z.string().optional(),
302
+ ogType: z.string().optional(),
303
+ ogSiteName: z.string().optional(),
304
+ twitterSite: z.string().optional(),
305
+ twitterCreator: z.string().optional(),
306
+ author: z.string().optional(),
307
+ keywords: z.string().optional(),
308
+ canonicalUrl: z.string().optional(),
309
+ language: z.string().optional(),
310
+ robots: z.string().optional()
311
+ });
312
+ var GalleryDataSchema = z.object({
313
+ title: z.string(),
314
+ description: z.string(),
315
+ mediaBasePath: z.string().optional(),
316
+ url: z.string().optional(),
317
+ headerImage: z.string(),
318
+ headerImageBlurHash: z.string().optional(),
319
+ thumbnailSize: z.number().optional(),
320
+ metadata: GalleryMetadataSchema,
321
+ mediaBaseUrl: z.string().optional(),
322
+ analyticsScript: z.string().optional(),
323
+ sections: z.array(GallerySectionSchema),
324
+ subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
325
+ });
326
+ var GalleryDataDeprecatedSchema = z.object({
327
+ title: z.string(),
328
+ description: z.string(),
329
+ url: z.string().optional(),
330
+ headerImage: z.string(),
331
+ thumbnailSize: z.number().optional(),
332
+ metadata: GalleryMetadataSchema,
333
+ mediaBaseUrl: z.string().optional(),
334
+ analyticsScript: z.string().optional(),
335
+ sections: z.array(GallerySectionDeprecatedSchema),
336
+ subGalleries: z.object({ title: z.string(), galleries: z.array(SubGallerySchema) })
337
+ });
338
+
339
+ // src/utils/gallery.ts
340
+ function parseGalleryJson(galleryJsonPath, ui) {
341
+ let galleryContent;
342
+ try {
343
+ galleryContent = fs8.readFileSync(galleryJsonPath, "utf8");
344
+ } catch (error) {
345
+ ui.error("Error parsing gallery.json: file not found");
346
+ throw error;
347
+ }
348
+ let galleryJSON;
349
+ try {
350
+ galleryJSON = JSON.parse(galleryContent);
351
+ } catch (error) {
352
+ ui.error("Error parsing gallery.json: invalid JSON");
353
+ throw error;
354
+ }
355
+ try {
356
+ return GalleryDataSchema.parse(galleryJSON);
357
+ } catch (error) {
358
+ try {
359
+ const deprecatedGalleryData = GalleryDataDeprecatedSchema.parse(galleryJSON);
360
+ return migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui);
361
+ } catch {
362
+ ui.error("Error parsing gallery.json: invalid gallery data");
363
+ throw error;
364
+ }
365
+ }
366
+ }
367
+ function migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui) {
368
+ ui.start("Old gallery.json format detected. Migrating gallery.json to the new data format.");
369
+ let mediaBasePath;
370
+ const imagePath = deprecatedGalleryData.sections[0].images[0].path;
371
+ if (imagePath && imagePath !== path7.join("..", path7.basename(imagePath))) {
372
+ mediaBasePath = path7.resolve(path7.join(path7.dirname(galleryJsonPath)), path7.dirname(imagePath));
373
+ }
374
+ const sections = deprecatedGalleryData.sections.map((section) => ({
375
+ ...section,
376
+ images: section.images.map((image) => ({
377
+ ...image,
378
+ path: void 0,
379
+ filename: path7.basename(image.path)
380
+ }))
381
+ }));
382
+ const galleryData = {
383
+ ...deprecatedGalleryData,
384
+ headerImage: path7.basename(deprecatedGalleryData.headerImage),
385
+ sections,
386
+ mediaBasePath
387
+ };
388
+ ui.debug("Backing up old gallery.json file");
389
+ fs8.copyFileSync(galleryJsonPath, `${galleryJsonPath}.old`);
390
+ ui.debug("Writing gallery data to gallery.json file");
391
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
392
+ ui.success("Gallery data migrated to the new data format successfully.");
393
+ return galleryData;
394
+ }
395
+ function getMediaFileType(fileName) {
396
+ const ext = path7.extname(fileName).toLowerCase();
397
+ if (IMAGE_EXTENSIONS.has(ext)) return "image";
398
+ if (VIDEO_EXTENSIONS.has(ext)) return "video";
399
+ return null;
400
+ }
401
+ function capitalizeTitle(folderName) {
402
+ return folderName.replace("-", " ").replace("_", " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
403
+ }
404
+
405
+ // src/modules/init/index.ts
406
+ async function scanDirectory(dirPath, ui) {
407
+ const mediaFiles = [];
408
+ const subGalleryDirectories = [];
409
+ try {
410
+ const entries = await promises.readdir(dirPath, { withFileTypes: true });
411
+ for (const entry of entries) {
412
+ if (entry.isFile()) {
413
+ const mediaType = getMediaFileType(entry.name);
414
+ if (mediaType) {
415
+ const mediaFile = {
416
+ type: mediaType,
417
+ filename: entry.name,
418
+ width: 0,
419
+ height: 0
420
+ };
421
+ mediaFiles.push(mediaFile);
422
+ }
423
+ } else if (entry.isDirectory() && entry.name !== "gallery") {
424
+ subGalleryDirectories.push(path7.join(dirPath, entry.name));
425
+ }
426
+ }
427
+ } catch (error) {
428
+ if (error instanceof Error && error.message.includes("ENOENT")) {
429
+ ui.error(`Directory does not exist: ${dirPath}`);
430
+ } else if (error instanceof Error && error.message.includes("ENOTDIR")) {
431
+ ui.error(`Path is not a directory: ${dirPath}`);
432
+ } else {
433
+ ui.error(`Error scanning directory ${dirPath}:`, error);
434
+ }
435
+ throw error;
436
+ }
437
+ return { mediaFiles, subGalleryDirectories };
438
+ }
439
+ async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
440
+ ui.info(`Enter gallery settings for the gallery in folder "${galleryName}"`);
441
+ const title = await ui.prompt("Enter gallery title", { type: "text", default: "My Gallery", placeholder: "My Gallery" });
442
+ const description = await ui.prompt("Enter gallery description", {
443
+ type: "text",
444
+ default: "My gallery with fantastic photos.",
445
+ placeholder: "My gallery with fantastic photos."
446
+ });
447
+ const url = await ui.prompt("Enter the URL where the gallery will be hosted (important for social media image)", {
448
+ type: "text",
449
+ default: "",
450
+ placeholder: ""
451
+ });
452
+ const headerImage = await ui.prompt("Enter the name of the header image", {
453
+ type: "text",
454
+ default: defaultImage,
455
+ placeholder: defaultImage
456
+ });
457
+ return { title, description, url, headerImage };
458
+ }
459
+ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ui) {
460
+ const galleryDir = path7.dirname(galleryJsonPath);
461
+ const isSameLocation = path7.relative(scanPath, path7.join(galleryDir, "..")) === "";
462
+ const mediaBasePath = isSameLocation ? void 0 : scanPath;
463
+ const relativeSubGalleries = subGalleries.map((subGallery) => ({
464
+ ...subGallery,
465
+ headerImage: subGallery.headerImage ? path7.relative(galleryDir, subGallery.headerImage) : ""
466
+ }));
467
+ let galleryData = {
468
+ title: "My Gallery",
469
+ description: "My gallery with fantastic photos.",
470
+ headerImage: mediaFiles[0]?.filename || "",
471
+ mediaBasePath,
472
+ metadata: {},
473
+ sections: [
474
+ {
475
+ images: mediaFiles
476
+ }
477
+ ],
478
+ subGalleries: {
479
+ title: "Sub Galleries",
480
+ galleries: relativeSubGalleries
481
+ }
482
+ };
483
+ if (!useDefaultSettings) {
484
+ galleryData = {
485
+ ...galleryData,
486
+ ...await getGallerySettingsFromUser(
487
+ path7.basename(path7.join(galleryDir, "..")),
488
+ path7.basename(mediaFiles[0]?.filename || ""),
489
+ ui
490
+ )
491
+ };
492
+ }
493
+ await promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
494
+ }
495
+ async function galleryExists(outputPath) {
496
+ const galleryPath = path7.join(outputPath, "gallery");
497
+ const galleryJsonPath = path7.join(galleryPath, "gallery.json");
498
+ try {
499
+ await promises.access(galleryJsonPath);
500
+ return true;
501
+ } catch {
502
+ return false;
503
+ }
504
+ }
505
+ async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ui) {
506
+ ui.start(`Scanning ${scanPath}`);
507
+ let totalFiles = 0;
508
+ let totalGalleries = 1;
509
+ const subGalleries = [];
510
+ const { mediaFiles, subGalleryDirectories } = await scanDirectory(scanPath, ui);
511
+ totalFiles += mediaFiles.length;
512
+ if (recursive) {
513
+ for (const subGalleryDir of subGalleryDirectories) {
514
+ const result2 = await processDirectory(
515
+ subGalleryDir,
516
+ path7.join(outputPath, path7.basename(subGalleryDir)),
517
+ recursive,
518
+ useDefaultSettings,
519
+ force,
520
+ ui
521
+ );
522
+ totalFiles += result2.totalFiles;
523
+ totalGalleries += result2.totalGalleries;
524
+ if (result2.subGallery) {
525
+ subGalleries.push(result2.subGallery);
526
+ }
527
+ }
528
+ }
529
+ if (mediaFiles.length > 0 || subGalleries.length > 0) {
530
+ const galleryPath = path7.join(outputPath, "gallery");
531
+ const galleryJsonPath = path7.join(galleryPath, "gallery.json");
532
+ const exists = await galleryExists(outputPath);
533
+ if (exists && !force) {
534
+ const shouldOverride = await ui.prompt(`Gallery already exists at ${galleryJsonPath}. Do you want to override it?`, {
535
+ type: "confirm",
536
+ default: false
537
+ });
538
+ if (!shouldOverride) {
539
+ ui.info("Skipping gallery creation");
540
+ return { totalFiles: 0, totalGalleries: 0 };
541
+ }
542
+ }
543
+ try {
544
+ await promises.mkdir(galleryPath, { recursive: true });
545
+ await createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries, useDefaultSettings, ui);
546
+ ui.success(
547
+ `Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
548
+ );
549
+ } catch (error) {
550
+ ui.error(`Error creating gallery.json at ${galleryJsonPath}`);
551
+ throw error;
552
+ }
553
+ }
554
+ const result = { totalFiles, totalGalleries };
555
+ if (mediaFiles.length > 0 || subGalleries.length > 0) {
556
+ const dirName = path7.basename(scanPath);
557
+ result.subGallery = {
558
+ title: capitalizeTitle(dirName),
559
+ headerImage: mediaFiles[0]?.filename || "",
560
+ path: path7.join("..", dirName)
561
+ };
562
+ }
563
+ return result;
564
+ }
565
+ async function init(options, ui) {
566
+ try {
567
+ const scanPath = path7.resolve(options.photos);
568
+ const outputPath = options.gallery ? path7.resolve(options.gallery) : scanPath;
569
+ const result = await processDirectory(scanPath, outputPath, options.recursive, options.default, options.force, ui);
570
+ ui.box(
571
+ `Created ${result.totalGalleries} ${result.totalGalleries === 1 ? "gallery" : "galleries"} with ${result.totalFiles} media ${result.totalFiles === 1 ? "file" : "files"}`
572
+ );
573
+ return {
574
+ processedMediaCount: result.totalFiles,
575
+ processedGalleryCount: result.totalGalleries
576
+ };
577
+ } catch (error) {
578
+ ui.error("Error initializing gallery");
579
+ throw error;
580
+ }
581
+ }
264
582
  async function getFileMtime(filePath) {
265
583
  const stats = await promises.stat(filePath);
266
584
  return stats.mtime;
267
585
  }
268
- async function generateBlurHash(imagePath, componentX = 4, componentY = 3) {
269
- const image = await loadImage(imagePath);
270
- const { data, info } = await image.resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
271
- const pixels = new Uint8ClampedArray(data.buffer);
272
- return encode(pixels, info.width, info.height, componentX, componentY);
273
- }
274
586
  async function getVideoDimensions(filePath) {
275
587
  const data = await ffprobe(filePath);
276
588
  const videoStream = data.streams.find((stream) => stream.codec_type === "video");
@@ -331,7 +643,7 @@ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, out
331
643
  // src/modules/thumbnails/index.ts
332
644
  async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
333
645
  const fileMtime = await getFileMtime(imagePath);
334
- if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs6.existsSync(thumbnailPath)) {
646
+ if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8.existsSync(thumbnailPath)) {
335
647
  return void 0;
336
648
  }
337
649
  const { image, metadata } = await loadImageWithMetadata(imagePath);
@@ -353,7 +665,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
353
665
  const blurHash = await generateBlurHash(thumbnailPath);
354
666
  return {
355
667
  type: "image",
356
- path: imagePath,
668
+ filename: path7.basename(imagePath),
357
669
  alt: description,
358
670
  width: imageDimensions.width,
359
671
  height: imageDimensions.height,
@@ -369,7 +681,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
369
681
  }
370
682
  async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
371
683
  const fileMtime = await getFileMtime(videoPath);
372
- if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs6.existsSync(thumbnailPath)) {
684
+ if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8.existsSync(thumbnailPath)) {
373
685
  return void 0;
374
686
  }
375
687
  const videoDimensions = await getVideoDimensions(videoPath);
@@ -384,7 +696,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
384
696
  const blurHash = await generateBlurHash(thumbnailPath);
385
697
  return {
386
698
  type: "video",
387
- path: videoPath,
699
+ filename: path7.basename(videoPath),
388
700
  alt: void 0,
389
701
  width: videoDimensions.width,
390
702
  height: videoDimensions.height,
@@ -398,24 +710,24 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
398
710
  lastMediaTimestamp: fileMtime.toISOString()
399
711
  };
400
712
  }
401
- async function processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnailSize, ui) {
713
+ async function processMediaFile(mediaFile, mediaBasePath, galleryDir, thumbnailsPath, thumbnailSize, ui) {
402
714
  try {
403
- const galleryJsonDir = path4.join(galleryDir, "gallery");
404
- const filePath = path4.resolve(path4.join(galleryJsonDir, mediaFile.path));
405
- const fileName = path4.basename(filePath);
406
- const fileNameWithoutExt = path4.parse(fileName).name;
715
+ const filePath = path7.resolve(path7.join(mediaBasePath, mediaFile.filename));
716
+ const fileName = mediaFile.filename;
717
+ const fileNameWithoutExt = path7.parse(fileName).name;
718
+ const galleryJsonDir = path7.join(galleryDir, "gallery");
407
719
  const thumbnailFileName = `${fileNameWithoutExt}.avif`;
408
- const thumbnailPath = path4.join(thumbnailsPath, thumbnailFileName);
720
+ const thumbnailPath = path7.join(thumbnailsPath, thumbnailFileName);
409
721
  const thumbnailPathRetina = thumbnailPath.replace(".avif", "@2x.avif");
410
- const relativeThumbnailPath = path4.relative(galleryJsonDir, thumbnailPath);
411
- const relativeThumbnailRetinaPath = path4.relative(galleryJsonDir, thumbnailPathRetina);
722
+ const relativeThumbnailPath = path7.relative(galleryJsonDir, thumbnailPath);
723
+ const relativeThumbnailRetinaPath = path7.relative(galleryJsonDir, thumbnailPathRetina);
412
724
  const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
413
725
  const verbose = ui.level === LogLevels.debug;
414
726
  ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
415
727
  const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
416
728
  if (!updatedMediaFile) {
417
729
  ui.debug(` Skipping ${fileName} because it has already been processed`);
418
- if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs6.existsSync(thumbnailPath)) {
730
+ if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs8.existsSync(thumbnailPath)) {
419
731
  try {
420
732
  const blurHash = await generateBlurHash(thumbnailPath);
421
733
  return {
@@ -431,34 +743,41 @@ async function processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnail
431
743
  }
432
744
  return mediaFile;
433
745
  }
434
- updatedMediaFile.path = mediaFile.path;
746
+ updatedMediaFile.filename = mediaFile.filename;
435
747
  if (updatedMediaFile.thumbnail) {
436
748
  updatedMediaFile.thumbnail.path = relativeThumbnailPath;
437
749
  updatedMediaFile.thumbnail.pathRetina = relativeThumbnailRetinaPath;
438
750
  }
439
751
  return updatedMediaFile;
440
752
  } catch (error) {
441
- handleFileProcessingError(error, path4.basename(mediaFile.path), ui);
442
- return mediaFile;
753
+ handleFileProcessingError(error, mediaFile.filename, ui);
754
+ return { ...mediaFile, thumbnail: void 0 };
443
755
  }
444
756
  }
445
757
  async function processGalleryThumbnails(galleryDir, ui) {
446
- const galleryJsonPath = path4.join(galleryDir, "gallery", "gallery.json");
447
- const thumbnailsPath = path4.join(galleryDir, "gallery", "images");
758
+ const galleryJsonPath = path7.join(galleryDir, "gallery", "gallery.json");
759
+ const thumbnailsPath = path7.join(galleryDir, "gallery", "images");
448
760
  ui.start(`Creating thumbnails: ${galleryDir}`);
449
761
  try {
450
- fs6.mkdirSync(thumbnailsPath, { recursive: true });
451
- const galleryContent = fs6.readFileSync(galleryJsonPath, "utf8");
452
- const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
762
+ fs8.mkdirSync(thumbnailsPath, { recursive: true });
763
+ const galleryData = parseGalleryJson(galleryJsonPath, ui);
453
764
  const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
765
+ const mediaBasePath = galleryData.mediaBasePath ?? path7.join(galleryDir);
454
766
  let processedCount = 0;
455
767
  for (const section of galleryData.sections) {
456
768
  for (const [index, mediaFile] of section.images.entries()) {
457
- section.images[index] = await processMediaFile(mediaFile, galleryDir, thumbnailsPath, thumbnailSize, ui);
769
+ section.images[index] = await processMediaFile(
770
+ mediaFile,
771
+ mediaBasePath,
772
+ galleryDir,
773
+ thumbnailsPath,
774
+ thumbnailSize,
775
+ ui
776
+ );
458
777
  }
459
778
  processedCount += section.images.length;
460
779
  }
461
- fs6.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
780
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
462
781
  ui.success(`Created thumbnails for ${processedCount} media files`);
463
782
  return processedCount;
464
783
  } catch (error) {
@@ -493,68 +812,113 @@ async function thumbnails(options, ui) {
493
812
  }
494
813
 
495
814
  // src/modules/build/index.ts
496
- function checkFileIsOneFolderUp(filePath) {
497
- const normalizedPath = path4.normalize(filePath);
498
- const pathParts = normalizedPath.split(path4.sep);
499
- return pathParts.length === 2 && pathParts[0] === "..";
500
- }
501
815
  function copyPhotos(galleryData, galleryDir, ui) {
502
816
  for (const section of galleryData.sections) {
503
817
  for (const image of section.images) {
504
- if (!checkFileIsOneFolderUp(image.path)) {
505
- const sourcePath = path4.join(galleryDir, "gallery", image.path);
506
- const fileName = path4.basename(image.path);
507
- const destPath = path4.join(galleryDir, fileName);
818
+ if (galleryData.mediaBasePath) {
819
+ const sourcePath = path7.join(galleryData.mediaBasePath, image.filename);
820
+ const destPath = path7.join(galleryDir, image.filename);
508
821
  ui.debug(`Copying photo to ${destPath}`);
509
- fs6.copyFileSync(sourcePath, destPath);
822
+ fs8.copyFileSync(sourcePath, destPath);
510
823
  }
511
824
  }
512
825
  }
513
826
  }
827
+ async function scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, ui) {
828
+ const scanPath = galleryData.mediaBasePath || galleryDir;
829
+ ui.debug(`Scanning ${scanPath} for new media files`);
830
+ let scanResult;
831
+ try {
832
+ scanResult = await scanDirectory(scanPath, ui);
833
+ } catch {
834
+ ui.debug(`Could not scan directory ${scanPath}`);
835
+ return galleryData;
836
+ }
837
+ const existingFilenames = new Set(
838
+ galleryData.sections.flatMap((section) => section.images.map((image) => image.filename))
839
+ );
840
+ const newMediaFiles = scanResult.mediaFiles.filter((file) => !existingFilenames.has(file.filename));
841
+ if (newMediaFiles.length > 0) {
842
+ ui.info(`Found ${newMediaFiles.length} new media ${newMediaFiles.length === 1 ? "file" : "files"}`);
843
+ if (galleryData.sections.length === 0) {
844
+ galleryData.sections.push({ images: [] });
845
+ }
846
+ const lastSectionIndex = galleryData.sections.length - 1;
847
+ const lastSection = galleryData.sections[lastSectionIndex];
848
+ lastSection.images.push(...newMediaFiles);
849
+ ui.debug("Updating gallery.json with new files");
850
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
851
+ ui.success(`Added ${newMediaFiles.length} new ${newMediaFiles.length === 1 ? "file" : "files"} to gallery.json`);
852
+ } else {
853
+ ui.debug("No new media files found");
854
+ }
855
+ return galleryData;
856
+ }
514
857
  async function buildGallery(galleryDir, templateDir, ui, baseUrl) {
515
858
  ui.start(`Building gallery ${galleryDir}`);
516
- await processGalleryThumbnails(galleryDir, ui);
517
- const galleryJsonPath = path4.join(galleryDir, "gallery", "gallery.json");
518
- const galleryContent = fs6.readFileSync(galleryJsonPath, "utf8");
519
- const galleryData = GalleryDataSchema.parse(JSON.parse(galleryContent));
520
- const socialMediaCardImagePath = path4.join(galleryDir, "gallery", "images", "social-media-card.jpg");
521
- const headerImagePath = path4.resolve(path4.join(galleryDir, "gallery"), galleryData.headerImage);
859
+ const galleryJsonPath = path7.join(galleryDir, "gallery", "gallery.json");
860
+ let galleryData = parseGalleryJson(galleryJsonPath, ui);
861
+ galleryData = await scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, ui);
862
+ const socialMediaCardImagePath = path7.join(galleryDir, "gallery", "images", "social-media-card.jpg");
863
+ const mediaBasePath = galleryData.mediaBasePath;
864
+ const mediaBaseUrl = baseUrl || galleryData.mediaBaseUrl;
865
+ const headerImagePath = mediaBasePath ? path7.join(mediaBasePath, galleryData.headerImage) : path7.resolve(galleryDir, galleryData.headerImage);
866
+ const imagesFolder = path7.join(galleryDir, "gallery", "images");
867
+ const currentHeaderBasename = path7.basename(headerImagePath, path7.extname(headerImagePath));
868
+ if (!fs8.existsSync(imagesFolder)) {
869
+ fs8.mkdirSync(imagesFolder, { recursive: true });
870
+ }
871
+ const headerImageChanged = hasOldHeaderImages(imagesFolder, currentHeaderBasename);
872
+ if (headerImageChanged) {
873
+ ui.info("Header image changed, cleaning up old assets");
874
+ cleanupOldHeaderImages(imagesFolder, currentHeaderBasename, ui);
875
+ if (fs8.existsSync(socialMediaCardImagePath)) {
876
+ fs8.unlinkSync(socialMediaCardImagePath);
877
+ ui.debug("Deleted old social media card");
878
+ }
879
+ }
522
880
  await createGallerySocialMediaCardImage(headerImagePath, galleryData.title, socialMediaCardImagePath, ui);
523
- galleryData.metadata.image = galleryData.metadata.image || `${galleryData.url || ""}/${path4.relative(galleryDir, socialMediaCardImagePath)}`;
524
- fs6.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
525
- await createOptimizedHeaderImage(headerImagePath, path4.join(galleryDir, "gallery", "images"), ui);
526
- if (!baseUrl) {
527
- const shouldCopyPhotos = galleryData.sections.some(
528
- (section) => section.images.some((image) => !checkFileIsOneFolderUp(image.path))
529
- );
530
- if (shouldCopyPhotos && await ui.prompt("All photos need to be copied. Are you sure you want to continue?", { type: "confirm" })) {
881
+ galleryData.metadata.image = galleryData.metadata.image || `${galleryData.url || ""}/${path7.relative(galleryDir, socialMediaCardImagePath)}`;
882
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
883
+ const { blurHash } = await createOptimizedHeaderImage(headerImagePath, imagesFolder, ui);
884
+ if (galleryData.headerImageBlurHash !== blurHash) {
885
+ ui.debug("Updating gallery.json with header image blurhash");
886
+ galleryData.headerImageBlurHash = blurHash;
887
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
888
+ }
889
+ if (!mediaBaseUrl && mediaBasePath) {
890
+ const shouldCopyPhotos = await ui.prompt("All photos need to be copied. Are you sure you want to continue?", {
891
+ type: "confirm"
892
+ });
893
+ if (shouldCopyPhotos) {
531
894
  ui.debug("Copying photos");
532
895
  copyPhotos(galleryData, galleryDir, ui);
533
896
  }
534
897
  }
535
- if (baseUrl) {
898
+ if (mediaBaseUrl && galleryData.mediaBaseUrl !== mediaBaseUrl) {
536
899
  ui.debug("Updating gallery.json with baseUrl");
537
- galleryData.mediaBaseUrl = baseUrl;
538
- fs6.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
900
+ galleryData.mediaBaseUrl = mediaBaseUrl;
901
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
539
902
  }
903
+ await processGalleryThumbnails(galleryDir, ui);
540
904
  ui.debug("Building gallery from template");
541
905
  try {
542
906
  process3.env.GALLERY_JSON_PATH = galleryJsonPath;
543
- process3.env.GALLERY_OUTPUT_DIR = path4.join(galleryDir, "gallery");
907
+ process3.env.GALLERY_OUTPUT_DIR = path7.join(galleryDir, "gallery");
544
908
  execSync("npx astro build", { cwd: templateDir, stdio: ui.level === LogLevels.debug ? "inherit" : "ignore" });
545
909
  } catch (error) {
546
910
  ui.error(`Build failed for ${galleryDir}`);
547
911
  throw error;
548
912
  }
549
- const outputDir = path4.join(galleryDir, "gallery");
550
- const buildDir = path4.join(outputDir, "_build");
913
+ const outputDir = path7.join(galleryDir, "gallery");
914
+ const buildDir = path7.join(outputDir, "_build");
551
915
  ui.debug(`Copying build output to ${outputDir}`);
552
- fs6.cpSync(buildDir, outputDir, { recursive: true });
916
+ fs8.cpSync(buildDir, outputDir, { recursive: true });
553
917
  ui.debug("Moving index.html to gallery directory");
554
- fs6.copyFileSync(path4.join(outputDir, "index.html"), path4.join(galleryDir, "index.html"));
555
- fs6.rmSync(path4.join(outputDir, "index.html"));
918
+ fs8.copyFileSync(path7.join(outputDir, "index.html"), path7.join(galleryDir, "index.html"));
919
+ fs8.rmSync(path7.join(outputDir, "index.html"));
556
920
  ui.debug("Cleaning up build directory");
557
- fs6.rmSync(buildDir, { recursive: true, force: true });
921
+ fs8.rmSync(buildDir, { recursive: true, force: true });
558
922
  ui.success(`Gallery built successfully`);
559
923
  }
560
924
  async function build(options, ui) {
@@ -565,11 +929,11 @@ async function build(options, ui) {
565
929
  return { processedGalleryCount: 0 };
566
930
  }
567
931
  const themePath = await import.meta.resolve("@simple-photo-gallery/theme-modern/package.json");
568
- const themeDir = path4.dirname(new URL(themePath).pathname);
932
+ const themeDir = path7.dirname(new URL(themePath).pathname);
569
933
  let totalGalleries = 0;
570
934
  for (const dir of galleryDirs) {
571
- const baseUrl = options.baseUrl ? `${options.baseUrl}${path4.relative(options.gallery, dir)}` : void 0;
572
- await buildGallery(path4.resolve(dir), themeDir, ui, baseUrl);
935
+ const baseUrl = options.baseUrl ? `${options.baseUrl}${path7.relative(options.gallery, dir)}` : void 0;
936
+ await buildGallery(path7.resolve(dir), themeDir, ui, baseUrl);
573
937
  ++totalGalleries;
574
938
  }
575
939
  ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
@@ -585,20 +949,20 @@ async function build(options, ui) {
585
949
  }
586
950
  async function cleanGallery(galleryDir, ui) {
587
951
  let filesRemoved = 0;
588
- const indexHtmlPath = path4.join(galleryDir, "index.html");
589
- if (fs6.existsSync(indexHtmlPath)) {
952
+ const indexHtmlPath = path7.join(galleryDir, "index.html");
953
+ if (fs8.existsSync(indexHtmlPath)) {
590
954
  try {
591
- fs6.rmSync(indexHtmlPath);
955
+ fs8.rmSync(indexHtmlPath);
592
956
  ui.debug(`Removed: ${indexHtmlPath}`);
593
957
  filesRemoved++;
594
958
  } catch (error) {
595
959
  ui.warn(`Failed to remove index.html: ${error}`);
596
960
  }
597
961
  }
598
- const galleryPath = path4.join(galleryDir, "gallery");
599
- if (fs6.existsSync(galleryPath)) {
962
+ const galleryPath = path7.join(galleryDir, "gallery");
963
+ if (fs8.existsSync(galleryPath)) {
600
964
  try {
601
- fs6.rmSync(galleryPath, { recursive: true, force: true });
965
+ fs8.rmSync(galleryPath, { recursive: true, force: true });
602
966
  ui.debug(`Removed directory: ${galleryPath}`);
603
967
  filesRemoved++;
604
968
  } catch (error) {
@@ -614,8 +978,8 @@ async function cleanGallery(galleryDir, ui) {
614
978
  }
615
979
  async function clean(options, ui) {
616
980
  try {
617
- const basePath = path4.resolve(options.gallery);
618
- if (!fs6.existsSync(basePath)) {
981
+ const basePath = path7.resolve(options.gallery);
982
+ if (!fs8.existsSync(basePath)) {
619
983
  ui.error(`Directory does not exist: ${basePath}`);
620
984
  return { processedGalleryCount: 0 };
621
985
  }
@@ -634,174 +998,6 @@ async function clean(options, ui) {
634
998
  throw error;
635
999
  }
636
1000
  }
637
- function getMediaFileType(fileName) {
638
- const ext = path4.extname(fileName).toLowerCase();
639
- if (IMAGE_EXTENSIONS.has(ext)) return "image";
640
- if (VIDEO_EXTENSIONS.has(ext)) return "video";
641
- return null;
642
- }
643
- function capitalizeTitle(folderName) {
644
- return folderName.replace("-", " ").replace("_", " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
645
- }
646
-
647
- // src/modules/init/index.ts
648
- async function scanDirectory(dirPath, ui) {
649
- const mediaFiles = [];
650
- const subGalleryDirectories = [];
651
- try {
652
- const entries = await promises.readdir(dirPath, { withFileTypes: true });
653
- for (const entry of entries) {
654
- if (entry.isFile()) {
655
- const fullPath = path4.join(dirPath, entry.name);
656
- const mediaType = getMediaFileType(entry.name);
657
- if (mediaType) {
658
- const mediaFile = {
659
- type: mediaType,
660
- path: fullPath,
661
- width: 0,
662
- height: 0
663
- };
664
- mediaFiles.push(mediaFile);
665
- }
666
- } else if (entry.isDirectory() && entry.name !== "gallery") {
667
- subGalleryDirectories.push(path4.join(dirPath, entry.name));
668
- }
669
- }
670
- } catch (error) {
671
- if (error instanceof Error && error.message.includes("ENOENT")) {
672
- ui.error(`Directory does not exist: ${dirPath}`);
673
- } else if (error instanceof Error && error.message.includes("ENOTDIR")) {
674
- ui.error(`Path is not a directory: ${dirPath}`);
675
- } else {
676
- ui.error(`Error scanning directory ${dirPath}:`, error);
677
- }
678
- throw error;
679
- }
680
- return { mediaFiles, subGalleryDirectories };
681
- }
682
- async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
683
- ui.info(`Enter gallery settings for the gallery in folder "${galleryName}"`);
684
- const title = await ui.prompt("Enter gallery title", { type: "text", default: "My Gallery", placeholder: "My Gallery" });
685
- const description = await ui.prompt("Enter gallery description", {
686
- type: "text",
687
- default: "My gallery with fantastic photos.",
688
- placeholder: "My gallery with fantastic photos."
689
- });
690
- const url = await ui.prompt("Enter the URL where the gallery will be hosted (important for social media image)", {
691
- type: "text",
692
- default: "",
693
- placeholder: ""
694
- });
695
- const headerImageName = await ui.prompt("Enter the name of the header image", {
696
- type: "text",
697
- default: defaultImage,
698
- placeholder: defaultImage
699
- });
700
- const headerImage = path4.join("..", headerImageName);
701
- return { title, description, url, headerImage };
702
- }
703
- async function createGalleryJson(mediaFiles, galleryJsonPath, subGalleries = [], useDefaultSettings, ui) {
704
- const galleryDir = path4.dirname(galleryJsonPath);
705
- const relativeMediaFiles = mediaFiles.map((file) => ({
706
- ...file,
707
- path: path4.relative(galleryDir, file.path)
708
- }));
709
- const relativeSubGalleries = subGalleries.map((subGallery) => ({
710
- ...subGallery,
711
- headerImage: subGallery.headerImage ? path4.relative(galleryDir, subGallery.headerImage) : ""
712
- }));
713
- let galleryData = {
714
- title: "My Gallery",
715
- description: "My gallery with fantastic photos.",
716
- headerImage: relativeMediaFiles[0]?.path || "",
717
- metadata: {},
718
- sections: [
719
- {
720
- images: relativeMediaFiles
721
- }
722
- ],
723
- subGalleries: {
724
- title: "Sub Galleries",
725
- galleries: relativeSubGalleries
726
- }
727
- };
728
- if (!useDefaultSettings) {
729
- galleryData = {
730
- ...galleryData,
731
- ...await getGallerySettingsFromUser(
732
- path4.basename(path4.join(galleryDir, "..")),
733
- path4.basename(mediaFiles[0]?.path || ""),
734
- ui
735
- )
736
- };
737
- }
738
- await promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
739
- }
740
- async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, ui) {
741
- ui.start(`Scanning ${scanPath}`);
742
- let totalFiles = 0;
743
- let totalGalleries = 1;
744
- const subGalleries = [];
745
- const { mediaFiles, subGalleryDirectories } = await scanDirectory(scanPath, ui);
746
- totalFiles += mediaFiles.length;
747
- if (recursive) {
748
- for (const subGalleryDir of subGalleryDirectories) {
749
- const result2 = await processDirectory(
750
- subGalleryDir,
751
- path4.join(outputPath, path4.basename(subGalleryDir)),
752
- recursive,
753
- useDefaultSettings,
754
- ui
755
- );
756
- totalFiles += result2.totalFiles;
757
- totalGalleries += result2.totalGalleries;
758
- if (result2.subGallery) {
759
- subGalleries.push(result2.subGallery);
760
- }
761
- }
762
- }
763
- if (mediaFiles.length > 0 || subGalleries.length > 0) {
764
- const galleryPath = path4.join(outputPath, "gallery");
765
- const galleryJsonPath = path4.join(galleryPath, "gallery.json");
766
- try {
767
- await promises.mkdir(galleryPath, { recursive: true });
768
- await createGalleryJson(mediaFiles, galleryJsonPath, subGalleries, useDefaultSettings, ui);
769
- ui.success(
770
- `Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
771
- );
772
- } catch (error) {
773
- ui.error(`Error creating gallery.json at ${galleryJsonPath}`);
774
- throw error;
775
- }
776
- }
777
- const result = { totalFiles, totalGalleries };
778
- if (mediaFiles.length > 0 || subGalleries.length > 0) {
779
- const dirName = path4.basename(scanPath);
780
- result.subGallery = {
781
- title: capitalizeTitle(dirName),
782
- headerImage: mediaFiles[0]?.path || "",
783
- path: path4.join("..", dirName)
784
- };
785
- }
786
- return result;
787
- }
788
- async function init(options, ui) {
789
- try {
790
- const scanPath = path4.resolve(options.photos);
791
- const outputPath = options.gallery ? path4.resolve(options.gallery) : scanPath;
792
- const result = await processDirectory(scanPath, outputPath, options.recursive, options.default, ui);
793
- ui.box(
794
- `Created ${result.totalGalleries} ${result.totalGalleries === 1 ? "gallery" : "galleries"} with ${result.totalFiles} media ${result.totalFiles === 1 ? "file" : "files"}`
795
- );
796
- return {
797
- processedMediaCount: result.totalFiles,
798
- processedGalleryCount: result.totalGalleries
799
- };
800
- } catch (error) {
801
- ui.error("Error initializing gallery");
802
- throw error;
803
- }
804
- }
805
1001
 
806
1002
  // src/modules/telemetry/index.ts
807
1003
  async function telemetry(options, ui, telemetryService2) {
@@ -1040,7 +1236,7 @@ async function waitForUpdateCheck(checkPromise) {
1040
1236
  // package.json
1041
1237
  var package_default = {
1042
1238
  name: "simple-photo-gallery",
1043
- version: "2.0.9"};
1239
+ version: "2.0.10-rc.1"};
1044
1240
 
1045
1241
  // src/index.ts
1046
1242
  var program = new Command();
@@ -1108,7 +1304,7 @@ program.command("init").description("Initialize a gallery by scaning a folder fo
1108
1304
  ).option(
1109
1305
  "-g, --gallery <path>",
1110
1306
  "Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
1111
- ).option("-r, --recursive", "Recursively create galleries from all photos subdirectories", false).option("-d, --default", "Use default gallery settings instead of asking the user", false).action(withCommandContext((options, ui) => init(options, ui)));
1307
+ ).option("-r, --recursive", "Recursively create galleries from all photos subdirectories", false).option("-d, --default", "Use default gallery settings instead of asking the user", false).option("-f, --force", "Force override existing galleries without asking", false).action(withCommandContext((options, ui) => init(options, ui)));
1112
1308
  program.command("thumbnails").description("Create thumbnails for all media files in the gallery").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).action(withCommandContext((options, ui) => thumbnails(options, ui)));
1113
1309
  program.command("build").description("Build the HTML gallery in the specified directory").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").action(withCommandContext((options, ui) => build(options, ui)));
1114
1310
  program.command("clean").description("Remove all gallery files and folders (index.html, gallery/)").option("-g, --gallery <path>", "Path to the directory of the gallery. Default: current working directory", process3.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));