simple-photo-gallery 2.0.18 → 2.1.0

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.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- var process3 = require('process');
4
+ var process4 = require('process');
5
5
  var commander = require('commander');
6
6
  var consola = require('consola');
7
7
  var child_process = require('child_process');
@@ -11,16 +11,19 @@ var buffer = require('buffer');
11
11
  var sharp2 = require('sharp');
12
12
  var blurhash = require('blurhash');
13
13
  var common = require('@simple-photo-gallery/common');
14
+ var theme = require('@simple-photo-gallery/common/theme');
14
15
  var ExifReader = require('exifreader');
15
16
  var ffprobe = require('node-ffprobe');
17
+ var url = require('url');
16
18
  var os = require('os');
17
19
  var Conf = require('conf');
18
20
  var axios = require('axios');
19
21
  var semverParser = require('semver-parser');
20
22
 
23
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
21
24
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
25
 
23
- var process3__default = /*#__PURE__*/_interopDefault(process3);
26
+ var process4__default = /*#__PURE__*/_interopDefault(process4);
24
27
  var fs8__default = /*#__PURE__*/_interopDefault(fs8);
25
28
  var path7__default = /*#__PURE__*/_interopDefault(path7);
26
29
  var sharp2__default = /*#__PURE__*/_interopDefault(sharp2);
@@ -31,7 +34,6 @@ var Conf__default = /*#__PURE__*/_interopDefault(Conf);
31
34
  var axios__default = /*#__PURE__*/_interopDefault(axios);
32
35
 
33
36
  // src/config/index.ts
34
- var DEFAULT_THUMBNAIL_SIZE = 300;
35
37
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".tif", ".svg", ".avif"]);
36
38
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v", ".3gp"]);
37
39
  var HEADER_IMAGE_LANDSCAPE_WIDTHS = [3840, 2560, 1920, 1280, 960, 640];
@@ -60,7 +62,7 @@ async function cropAndResizeImage(image, outputPath, width, height, format = "av
60
62
  withoutEnlargement: true
61
63
  }).toFormat(format).toFile(outputPath);
62
64
  }
63
- async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size) {
65
+ async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size, sizeDimension = "auto") {
64
66
  const originalWidth = metadata.width || 0;
65
67
  const originalHeight = metadata.height || 0;
66
68
  if (originalWidth === 0 || originalHeight === 0) {
@@ -69,12 +71,20 @@ async function createImageThumbnails(image, metadata, outputPath, outputPathReti
69
71
  const aspectRatio = originalWidth / originalHeight;
70
72
  let width;
71
73
  let height;
72
- if (originalWidth > originalHeight) {
74
+ if (sizeDimension === "width") {
73
75
  width = size;
74
76
  height = Math.round(size / aspectRatio);
75
- } else {
77
+ } else if (sizeDimension === "height") {
76
78
  width = Math.round(size * aspectRatio);
77
79
  height = size;
80
+ } else {
81
+ if (originalWidth > originalHeight) {
82
+ width = size;
83
+ height = Math.round(size / aspectRatio);
84
+ } else {
85
+ width = Math.round(size * aspectRatio);
86
+ height = size;
87
+ }
78
88
  }
79
89
  await resizeImage(image, outputPath, width, height);
80
90
  await resizeImage(image, outputPathRetina, width * 2, height * 2);
@@ -340,11 +350,15 @@ function migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui) {
340
350
  filename: path7__default.default.basename(image.path)
341
351
  }))
342
352
  }));
353
+ const thumbnails2 = deprecatedGalleryData.thumbnailSize === void 0 ? void 0 : { size: deprecatedGalleryData.thumbnailSize };
343
354
  const galleryData = {
344
355
  ...deprecatedGalleryData,
356
+ thumbnailSize: void 0,
357
+ // Remove old field
345
358
  headerImage: path7__default.default.basename(deprecatedGalleryData.headerImage),
346
359
  sections,
347
- mediaBasePath
360
+ mediaBasePath,
361
+ thumbnails: thumbnails2
348
362
  };
349
363
  ui.debug("Backing up old gallery.json file");
350
364
  fs8__default.default.copyFileSync(galleryJsonPath, `${galleryJsonPath}.old`);
@@ -415,9 +429,18 @@ async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
415
429
  default: defaultImage,
416
430
  placeholder: defaultImage
417
431
  });
418
- return { title, description, url, headerImage };
432
+ return {
433
+ title,
434
+ description,
435
+ url,
436
+ headerImage,
437
+ thumbnails: {
438
+ size: "",
439
+ edge: void 0
440
+ }
441
+ };
419
442
  }
420
- async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ctaBanner, ui) {
443
+ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ctaBanner, configOptions, ui) {
421
444
  const galleryDir = path7__default.default.dirname(galleryJsonPath);
422
445
  const isSameLocation = path7__default.default.relative(scanPath, path7__default.default.join(galleryDir, "..")) === "";
423
446
  const mediaBasePath = isSameLocation ? void 0 : scanPath;
@@ -425,12 +448,23 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalle
425
448
  ...subGallery,
426
449
  headerImage: subGallery.headerImage ? path7__default.default.relative(galleryDir, subGallery.headerImage) : ""
427
450
  }));
451
+ const thumbnailsConfig = {};
452
+ if (configOptions.thumbnailSize !== void 0) {
453
+ thumbnailsConfig.size = configOptions.thumbnailSize;
454
+ }
455
+ if (configOptions.thumbnailEdge !== void 0) {
456
+ thumbnailsConfig.edge = configOptions.thumbnailEdge;
457
+ }
428
458
  let galleryData = {
429
459
  title: "My Gallery",
430
460
  description: "My gallery with fantastic photos.",
431
461
  headerImage: mediaFiles[0]?.filename || "",
432
462
  mediaBasePath,
433
463
  metadata: {},
464
+ // Include theme if provided via CLI
465
+ ...configOptions.theme && { theme: configOptions.theme },
466
+ // Include thumbnails if any values were set via CLI
467
+ ...Object.keys(thumbnailsConfig).length > 0 && { thumbnails: thumbnailsConfig },
434
468
  sections: [
435
469
  {
436
470
  images: mediaFiles
@@ -443,13 +477,15 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalle
443
477
  ...ctaBanner !== void 0 && { ctaBanner }
444
478
  };
445
479
  if (!useDefaultSettings) {
480
+ const userSettings = await getGallerySettingsFromUser(
481
+ path7__default.default.basename(path7__default.default.join(galleryDir, "..")),
482
+ path7__default.default.basename(mediaFiles[0]?.filename || ""),
483
+ ui
484
+ );
485
+ const { thumbnails: thumbnails2, ...otherSettings } = userSettings;
446
486
  galleryData = {
447
487
  ...galleryData,
448
- ...await getGallerySettingsFromUser(
449
- path7__default.default.basename(path7__default.default.join(galleryDir, "..")),
450
- path7__default.default.basename(mediaFiles[0]?.filename || ""),
451
- ui
452
- )
488
+ ...otherSettings
453
489
  };
454
490
  }
455
491
  await fs8.promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
@@ -464,7 +500,7 @@ async function galleryExists(outputPath) {
464
500
  return false;
465
501
  }
466
502
  }
467
- async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ctaBanner, ui) {
503
+ async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ctaBanner, configOptions, ui) {
468
504
  ui.start(`Scanning ${scanPath}`);
469
505
  let totalFiles = 0;
470
506
  let totalGalleries = 1;
@@ -480,6 +516,7 @@ async function processDirectory(scanPath, outputPath, recursive, useDefaultSetti
480
516
  useDefaultSettings,
481
517
  force,
482
518
  ctaBanner,
519
+ configOptions,
483
520
  ui
484
521
  );
485
522
  totalFiles += result2.totalFiles;
@@ -505,7 +542,16 @@ async function processDirectory(scanPath, outputPath, recursive, useDefaultSetti
505
542
  }
506
543
  try {
507
544
  await fs8.promises.mkdir(galleryPath, { recursive: true });
508
- await createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries, useDefaultSettings, ctaBanner, ui);
545
+ await createGalleryJson(
546
+ mediaFiles,
547
+ galleryJsonPath,
548
+ scanPath,
549
+ subGalleries,
550
+ useDefaultSettings,
551
+ ctaBanner,
552
+ configOptions,
553
+ ui
554
+ );
509
555
  ui.success(
510
556
  `Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
511
557
  );
@@ -529,6 +575,11 @@ async function init(options, ui) {
529
575
  try {
530
576
  const scanPath = path7__default.default.resolve(options.photos);
531
577
  const outputPath = options.gallery ? path7__default.default.resolve(options.gallery) : scanPath;
578
+ const configOptions = {
579
+ theme: options.theme,
580
+ thumbnailSize: options.thumbnailSize,
581
+ thumbnailEdge: options.thumbnailEdge
582
+ };
532
583
  const result = await processDirectory(
533
584
  scanPath,
534
585
  outputPath,
@@ -536,6 +587,7 @@ async function init(options, ui) {
536
587
  options.default,
537
588
  options.force,
538
589
  options.ctaBanner,
590
+ configOptions,
539
591
  ui
540
592
  );
541
593
  ui.box(
@@ -585,9 +637,25 @@ async function getVideoDimensions(filePath) {
585
637
  }
586
638
  return dimensions;
587
639
  }
588
- async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
640
+ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, size, sizeDimension = "auto", verbose = false) {
589
641
  const aspectRatio = videoDimensions.width / videoDimensions.height;
590
- const width = Math.round(height * aspectRatio);
642
+ let width;
643
+ let height;
644
+ if (sizeDimension === "width") {
645
+ width = size;
646
+ height = Math.round(size / aspectRatio);
647
+ } else if (sizeDimension === "height") {
648
+ width = Math.round(size * aspectRatio);
649
+ height = size;
650
+ } else {
651
+ if (videoDimensions.width > videoDimensions.height) {
652
+ width = size;
653
+ height = Math.round(size / aspectRatio);
654
+ } else {
655
+ width = Math.round(size * aspectRatio);
656
+ height = size;
657
+ }
658
+ }
591
659
  const tempFramePath = `${outputPath}.temp.png`;
592
660
  return new Promise((resolve, reject) => {
593
661
  const ffmpeg = child_process.spawn("ffmpeg", [
@@ -628,7 +696,7 @@ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, out
628
696
  }
629
697
 
630
698
  // src/modules/thumbnails/index.ts
631
- async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
699
+ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, thumbnailSizeDimension = "auto", lastMediaTimestamp) {
632
700
  const fileMtime = await getFileMtime(imagePath);
633
701
  if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8__default.default.existsSync(thumbnailPath)) {
634
702
  return void 0;
@@ -647,7 +715,8 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
647
715
  metadata,
648
716
  thumbnailPath,
649
717
  thumbnailPathRetina,
650
- thumbnailSize
718
+ thumbnailSize,
719
+ thumbnailSizeDimension
651
720
  );
652
721
  const blurHash = await generateBlurHash(thumbnailPath);
653
722
  return {
@@ -666,7 +735,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
666
735
  lastMediaTimestamp: fileMtime.toISOString()
667
736
  };
668
737
  }
669
- async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
738
+ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, thumbnailSizeDimension = "auto", verbose, lastMediaTimestamp) {
670
739
  const fileMtime = await getFileMtime(videoPath);
671
740
  if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8__default.default.existsSync(thumbnailPath)) {
672
741
  return void 0;
@@ -678,6 +747,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
678
747
  thumbnailPath,
679
748
  thumbnailPathRetina,
680
749
  thumbnailSize,
750
+ thumbnailSizeDimension,
681
751
  verbose
682
752
  );
683
753
  const blurHash = await generateBlurHash(thumbnailPath);
@@ -697,7 +767,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
697
767
  lastMediaTimestamp: fileMtime.toISOString()
698
768
  };
699
769
  }
700
- async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailSize, ui) {
770
+ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailConfig, ui) {
701
771
  try {
702
772
  const filePath = path7__default.default.resolve(path7__default.default.join(mediaBasePath, mediaFile.filename));
703
773
  const fileName = mediaFile.filename;
@@ -708,7 +778,22 @@ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbn
708
778
  const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
709
779
  const verbose = ui.level === consola.LogLevels.debug;
710
780
  ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
711
- const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
781
+ const updatedMediaFile = await (mediaFile.type === "image" ? processImage(
782
+ filePath,
783
+ thumbnailPath,
784
+ thumbnailPathRetina,
785
+ thumbnailConfig.size,
786
+ thumbnailConfig.edge,
787
+ lastMediaTimestamp
788
+ ) : processVideo(
789
+ filePath,
790
+ thumbnailPath,
791
+ thumbnailPathRetina,
792
+ thumbnailConfig.size,
793
+ thumbnailConfig.edge,
794
+ verbose,
795
+ lastMediaTimestamp
796
+ ));
712
797
  if (!updatedMediaFile) {
713
798
  ui.debug(` Skipping ${fileName} because it has already been processed`);
714
799
  if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs8__default.default.existsSync(thumbnailPath)) {
@@ -744,19 +829,30 @@ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbn
744
829
  return { ...mediaFile, thumbnail: void 0 };
745
830
  }
746
831
  }
747
- async function processGalleryThumbnails(galleryDir, ui) {
832
+ async function processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig) {
748
833
  const galleryJsonPath = path7__default.default.join(galleryDir, "gallery", "gallery.json");
749
834
  const thumbnailsPath = path7__default.default.join(galleryDir, "gallery", "images");
750
835
  ui.start(`Creating thumbnails: ${galleryDir}`);
751
836
  try {
752
837
  fs8__default.default.mkdirSync(thumbnailsPath, { recursive: true });
753
838
  const galleryData = parseGalleryJson(galleryJsonPath, ui);
754
- const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
839
+ const galleryThumbnailConfig = theme.extractThumbnailConfigFromGallery(galleryData);
840
+ let themeConfig;
841
+ if (galleryData.theme) {
842
+ try {
843
+ const themeDir = await resolveThemeDir(galleryData.theme, ui);
844
+ themeConfig = theme.loadThemeConfig(themeDir);
845
+ } catch {
846
+ ui.debug(`Could not load theme config from ${galleryData.theme}, using defaults`);
847
+ }
848
+ }
849
+ const thumbnailConfig = theme.mergeThumbnailConfig(cliThumbnailConfig, galleryThumbnailConfig, themeConfig);
850
+ ui.debug(`Thumbnail config: size=${thumbnailConfig.size}, edge=${thumbnailConfig.edge}`);
755
851
  const mediaBasePath = galleryData.mediaBasePath ?? path7__default.default.join(galleryDir);
756
852
  let processedCount = 0;
757
853
  for (const section of galleryData.sections) {
758
854
  for (const [index, mediaFile] of section.images.entries()) {
759
- section.images[index] = await processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailSize, ui);
855
+ section.images[index] = await processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailConfig, ui);
760
856
  }
761
857
  processedCount += section.images.length;
762
858
  }
@@ -775,10 +871,11 @@ async function thumbnails(options, ui) {
775
871
  ui.error("No galleries found.");
776
872
  return { processedGalleryCount: 0, processedMediaCount: 0 };
777
873
  }
874
+ const cliThumbnailConfig = options.thumbnailSize !== void 0 || options.thumbnailEdge !== void 0 ? { size: options.thumbnailSize, edge: options.thumbnailEdge } : void 0;
778
875
  let totalGalleries = 0;
779
876
  let totalProcessed = 0;
780
877
  for (const galleryDir of galleryDirs) {
781
- const processed = await processGalleryThumbnails(galleryDir, ui);
878
+ const processed = await processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig);
782
879
  if (processed > 0) {
783
880
  ++totalGalleries;
784
881
  totalProcessed += processed;
@@ -837,7 +934,7 @@ async function scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, u
837
934
  }
838
935
  return galleryData;
839
936
  }
840
- async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl) {
937
+ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl, cliThumbnailConfig, cliTheme) {
841
938
  ui.start(`Building gallery ${galleryDir}`);
842
939
  const galleryJsonPath = path7__default.default.join(galleryDir, "gallery", "gallery.json");
843
940
  let galleryData = parseGalleryJson(galleryJsonPath, ui);
@@ -890,18 +987,35 @@ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnail
890
987
  galleryData.thumbsBaseUrl = thumbsBaseUrl;
891
988
  fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
892
989
  }
990
+ if (cliTheme && galleryData.theme !== cliTheme) {
991
+ ui.debug("Updating gallery.json with theme");
992
+ galleryData.theme = cliTheme;
993
+ fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
994
+ }
995
+ if (cliThumbnailConfig) {
996
+ const needsUpdate = cliThumbnailConfig.size !== void 0 && galleryData.thumbnails?.size !== cliThumbnailConfig.size || cliThumbnailConfig.edge !== void 0 && galleryData.thumbnails?.edge !== cliThumbnailConfig.edge;
997
+ if (needsUpdate) {
998
+ ui.debug("Updating gallery.json with thumbnail settings");
999
+ galleryData.thumbnails = {
1000
+ ...galleryData.thumbnails,
1001
+ ...cliThumbnailConfig.size !== void 0 && { size: cliThumbnailConfig.size },
1002
+ ...cliThumbnailConfig.edge !== void 0 && { edge: cliThumbnailConfig.edge }
1003
+ };
1004
+ fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
1005
+ }
1006
+ }
893
1007
  if (!galleryData.metadata.image) {
894
1008
  ui.debug("Updating gallery.json with social media card URL");
895
1009
  galleryData.metadata.image = thumbsBaseUrl ? `${thumbsBaseUrl}/${path7__default.default.basename(socialMediaCardImagePath)}` : `${galleryData.url || ""}/${path7__default.default.relative(galleryDir, socialMediaCardImagePath)}`;
896
1010
  fs8__default.default.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
897
1011
  }
898
1012
  if (shouldCreateThumbnails) {
899
- await processGalleryThumbnails(galleryDir, ui);
1013
+ await processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig);
900
1014
  }
901
1015
  ui.debug("Building gallery from template");
902
1016
  try {
903
- process3__default.default.env.GALLERY_JSON_PATH = galleryJsonPath;
904
- process3__default.default.env.GALLERY_OUTPUT_DIR = path7__default.default.join(galleryDir, "gallery");
1017
+ process4__default.default.env.GALLERY_JSON_PATH = galleryJsonPath;
1018
+ process4__default.default.env.GALLERY_OUTPUT_DIR = path7__default.default.join(galleryDir, "gallery");
905
1019
  child_process.execSync("npx astro build", { cwd: templateDir, stdio: ui.level === consola.LogLevels.debug ? "inherit" : "ignore" });
906
1020
  } catch (error) {
907
1021
  ui.error(`Build failed for ${galleryDir}`);
@@ -918,6 +1032,25 @@ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnail
918
1032
  fs8__default.default.rmSync(buildDir, { recursive: true, force: true });
919
1033
  ui.success(`Gallery built successfully`);
920
1034
  }
1035
+ function isLocalThemePath(theme) {
1036
+ return theme.startsWith("./") || theme.startsWith("../") || theme.startsWith("/");
1037
+ }
1038
+ async function resolveThemeDir(theme, ui) {
1039
+ if (isLocalThemePath(theme)) {
1040
+ const themeDir = path7__default.default.resolve(theme);
1041
+ const packageJsonPath = path7__default.default.join(themeDir, "package.json");
1042
+ if (!fs8__default.default.existsSync(packageJsonPath)) {
1043
+ throw new Error(`Theme directory not found or invalid: ${themeDir}. package.json not found.`);
1044
+ }
1045
+ ui.debug(`Using local theme: ${themeDir}`);
1046
+ return themeDir;
1047
+ } else {
1048
+ const themePath = await undefined(`${theme}/package.json`);
1049
+ const themeDir = path7__default.default.dirname(new URL(themePath).pathname);
1050
+ ui.debug(`Using npm theme package: ${theme} (${themeDir})`);
1051
+ return themeDir;
1052
+ }
1053
+ }
921
1054
  async function build(options, ui) {
922
1055
  try {
923
1056
  const galleryDirs = findGalleries(options.gallery, options.recursive);
@@ -925,20 +1058,41 @@ async function build(options, ui) {
925
1058
  ui.error("No galleries found.");
926
1059
  return { processedGalleryCount: 0 };
927
1060
  }
928
- const themePath = await undefined("@simple-photo-gallery/theme-modern/package.json");
929
- const themeDir = path7__default.default.dirname(new URL(themePath).pathname);
1061
+ const cliThumbnailConfig = options.thumbnailSize !== void 0 || options.thumbnailEdge !== void 0 ? { size: options.thumbnailSize, edge: options.thumbnailEdge } : void 0;
930
1062
  let totalGalleries = 0;
931
1063
  for (const dir of galleryDirs) {
1064
+ const galleryJsonPath = path7__default.default.join(dir, "gallery", "gallery.json");
1065
+ const galleryData = parseGalleryJson(galleryJsonPath, ui);
1066
+ const themeIdentifier = options.theme || galleryData.theme || "@simple-photo-gallery/theme-modern";
1067
+ const themeDir = await resolveThemeDir(themeIdentifier, ui);
932
1068
  const baseUrl = options.baseUrl ? `${options.baseUrl}${path7__default.default.relative(options.gallery, dir)}` : void 0;
933
1069
  const thumbsBaseUrl = options.thumbsBaseUrl ? `${options.thumbsBaseUrl}${path7__default.default.relative(options.gallery, dir)}` : void 0;
934
- await buildGallery(path7__default.default.resolve(dir), themeDir, options.scan, options.thumbnails, ui, baseUrl, thumbsBaseUrl);
1070
+ await buildGallery(
1071
+ path7__default.default.resolve(dir),
1072
+ themeDir,
1073
+ options.scan,
1074
+ options.thumbnails,
1075
+ ui,
1076
+ baseUrl,
1077
+ thumbsBaseUrl,
1078
+ cliThumbnailConfig,
1079
+ options.theme
1080
+ );
935
1081
  ++totalGalleries;
936
1082
  }
937
1083
  ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
938
1084
  return { processedGalleryCount: totalGalleries };
939
1085
  } catch (error) {
940
- if (error instanceof Error && error.message.includes("Cannot find package")) {
941
- ui.error("Theme package not found: @simple-photo-gallery/theme-modern/package.json");
1086
+ if (error instanceof Error) {
1087
+ if (error.message.includes("Cannot find package")) {
1088
+ ui.error(
1089
+ `Theme package not found: ${options.theme || "@simple-photo-gallery/theme-modern"}. Make sure it's installed.`
1090
+ );
1091
+ } else if (error.message.includes("Theme directory not found") || error.message.includes("package.json not found")) {
1092
+ ui.error(error.message);
1093
+ } else {
1094
+ ui.error("Error building gallery");
1095
+ }
942
1096
  } else {
943
1097
  ui.error("Error building gallery");
944
1098
  }
@@ -996,6 +1150,162 @@ async function clean(options, ui) {
996
1150
  throw error;
997
1151
  }
998
1152
  }
1153
+ function findMonorepoRoot(startDir) {
1154
+ let dir = path7__default.default.resolve(startDir);
1155
+ while (true) {
1156
+ const pkgPath = path7__default.default.join(dir, "package.json");
1157
+ if (fs8__default.default.existsSync(pkgPath)) {
1158
+ try {
1159
+ const pkg = JSON.parse(fs8__default.default.readFileSync(pkgPath, "utf8"));
1160
+ if (pkg && typeof pkg === "object" && "workspaces" in pkg) {
1161
+ return dir;
1162
+ }
1163
+ } catch {
1164
+ }
1165
+ }
1166
+ const parent = path7__default.default.dirname(dir);
1167
+ if (parent === dir) {
1168
+ return void 0;
1169
+ }
1170
+ dir = parent;
1171
+ }
1172
+ }
1173
+ function validateThemeName(name) {
1174
+ if (!name || name.trim().length === 0) {
1175
+ throw new Error("Theme name cannot be empty");
1176
+ }
1177
+ if (!/^[a-z0-9-]+$/i.test(name)) {
1178
+ throw new Error("Theme name can only contain letters, numbers, and hyphens");
1179
+ }
1180
+ return true;
1181
+ }
1182
+ async function ensureDirectory(dirPath, ui) {
1183
+ try {
1184
+ await fs8__default.default.promises.mkdir(dirPath, { recursive: true });
1185
+ ui.debug(`Created directory: ${dirPath}`);
1186
+ } catch (error) {
1187
+ if (error instanceof Error && "code" in error && error.code !== "EEXIST") {
1188
+ throw new Error(`Failed to create directory ${dirPath}: ${error.message}`);
1189
+ }
1190
+ }
1191
+ }
1192
+ var EXCLUDE_PATTERNS = ["node_modules", ".astro", "dist", "_build", ".git", "*.log", ".DS_Store"];
1193
+ function shouldExclude(name) {
1194
+ if (name === "README.md" || name === "README_BASE.md") {
1195
+ return true;
1196
+ }
1197
+ return EXCLUDE_PATTERNS.some((pattern) => {
1198
+ if (pattern.includes("*")) {
1199
+ const regexPattern = pattern.split("*").join(".*");
1200
+ const regex = new RegExp(regexPattern);
1201
+ return regex.test(name);
1202
+ }
1203
+ return name === pattern;
1204
+ });
1205
+ }
1206
+ async function copyDirectory(src, dest, ui) {
1207
+ await fs8__default.default.promises.mkdir(dest, { recursive: true });
1208
+ const entries = await fs8__default.default.promises.readdir(src, { withFileTypes: true });
1209
+ for (const entry of entries) {
1210
+ if (shouldExclude(entry.name)) {
1211
+ ui.debug(`Skipping excluded file/directory: ${entry.name}`);
1212
+ continue;
1213
+ }
1214
+ const srcPath = path7__default.default.join(src, entry.name);
1215
+ const destPath = path7__default.default.join(dest, entry.name);
1216
+ if (entry.isDirectory()) {
1217
+ await copyDirectory(srcPath, destPath, ui);
1218
+ } else {
1219
+ await fs8__default.default.promises.copyFile(srcPath, destPath);
1220
+ ui.debug(`Copied file: ${destPath}`);
1221
+ }
1222
+ }
1223
+ }
1224
+ function findBaseThemePath() {
1225
+ const moduleDir = path7__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
1226
+ const bundledTemplatePath = path7__default.default.resolve(moduleDir, "../../../src/modules/create-theme/templates/base");
1227
+ if (fs8__default.default.existsSync(bundledTemplatePath)) {
1228
+ return bundledTemplatePath;
1229
+ }
1230
+ const monorepoRoot = findMonorepoRoot(process4__default.default.cwd());
1231
+ const workspaceRoot = monorepoRoot ?? process4__default.default.cwd();
1232
+ const workspaceBaseThemePath = path7__default.default.join(workspaceRoot, "themes", "base");
1233
+ if (fs8__default.default.existsSync(workspaceBaseThemePath)) {
1234
+ return workspaceBaseThemePath;
1235
+ }
1236
+ throw new Error(
1237
+ `Base theme template not found. Tried:
1238
+ - ${bundledTemplatePath}
1239
+ - ${workspaceBaseThemePath}
1240
+
1241
+ Please ensure the templates are included in the package or themes/base exists in the workspace.`
1242
+ );
1243
+ }
1244
+ async function updatePackageJson(themeDir, themeName, ui) {
1245
+ const packageJsonPath = path7__default.default.join(themeDir, "package.json");
1246
+ const packageJsonContent = await fs8__default.default.promises.readFile(packageJsonPath, "utf8");
1247
+ const packageJson = JSON.parse(packageJsonContent);
1248
+ packageJson.name = themeName;
1249
+ await fs8__default.default.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
1250
+ ui.debug(`Updated package.json with theme name: ${themeName}`);
1251
+ }
1252
+ async function createReadmeFromBase(baseThemePath, themeDir, themeName, ui) {
1253
+ const readmeBasePath = path7__default.default.join(baseThemePath, "README_BASE.md");
1254
+ const readmePath = path7__default.default.join(themeDir, "README.md");
1255
+ if (!fs8__default.default.existsSync(readmeBasePath)) {
1256
+ throw new Error(`README_BASE.md not found in template: ${readmeBasePath}`);
1257
+ }
1258
+ let readme = await fs8__default.default.promises.readFile(readmeBasePath, "utf8");
1259
+ const displayName = themeName.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1260
+ readme = readme.replaceAll("{THEME_NAME}", displayName);
1261
+ readme = readme.replaceAll("{THEME_NAME_LOWER}", displayName.toLowerCase());
1262
+ await fs8__default.default.promises.writeFile(readmePath, readme, "utf8");
1263
+ ui.debug(`Created README.md from README_BASE.md for theme: ${themeName}`);
1264
+ }
1265
+ async function createTheme(options, ui) {
1266
+ try {
1267
+ validateThemeName(options.name);
1268
+ let themeDir;
1269
+ if (options.path) {
1270
+ themeDir = path7__default.default.resolve(options.path);
1271
+ } else {
1272
+ const monorepoRoot = findMonorepoRoot(process4__default.default.cwd());
1273
+ const baseDir = monorepoRoot ?? process4__default.default.cwd();
1274
+ const themesBaseDir = path7__default.default.resolve(baseDir, "themes");
1275
+ themeDir = path7__default.default.join(themesBaseDir, options.name);
1276
+ if (!fs8__default.default.existsSync(themesBaseDir)) {
1277
+ await ensureDirectory(themesBaseDir, ui);
1278
+ }
1279
+ }
1280
+ if (fs8__default.default.existsSync(themeDir)) {
1281
+ throw new Error(`Theme directory already exists: ${themeDir}. Cannot overwrite existing theme.`);
1282
+ }
1283
+ ui.start(`Creating theme: ${options.name}`);
1284
+ const baseThemePath = findBaseThemePath();
1285
+ ui.debug(`Using base theme from: ${baseThemePath}`);
1286
+ ui.debug("Copying base theme files...");
1287
+ await copyDirectory(baseThemePath, themeDir, ui);
1288
+ ui.debug("Updating theme-specific files...");
1289
+ await updatePackageJson(themeDir, options.name, ui);
1290
+ await createReadmeFromBase(baseThemePath, themeDir, options.name, ui);
1291
+ ui.success(`Theme created successfully at: ${themeDir}`);
1292
+ ui.info(`
1293
+ Next steps:`);
1294
+ ui.info(`1. cd ${themeDir}`);
1295
+ ui.info(`2. yarn install`);
1296
+ ui.info(`3. Customize your theme in src/pages/index.astro`);
1297
+ ui.info(`4. Initialize a gallery (run from directory with your images): spg init -p <images-folder>`);
1298
+ ui.info(`5. Build a gallery with your theme: spg build --theme ${themeDir} -g <gallery-folder>`);
1299
+ return { processedGalleryCount: 0 };
1300
+ } catch (error) {
1301
+ if (error instanceof Error) {
1302
+ ui.error(error.message);
1303
+ } else {
1304
+ ui.error("Failed to create theme");
1305
+ }
1306
+ throw error;
1307
+ }
1308
+ }
999
1309
 
1000
1310
  // src/modules/telemetry/index.ts
1001
1311
  async function telemetry(options, ui, telemetryService2) {
@@ -1025,7 +1335,7 @@ var ApiTelemetryClient = class {
1025
1335
  axios__default.default.post(this.endpoint, event, {
1026
1336
  headers: {
1027
1337
  "content-type": "application/json",
1028
- "user-agent": `simple-photo-gallery/${event.packageVersion} (${process3__default.default.platform}; ${process3__default.default.arch})`
1338
+ "user-agent": `simple-photo-gallery/${event.packageVersion} (${process4__default.default.platform}; ${process4__default.default.arch})`
1029
1339
  }
1030
1340
  });
1031
1341
  } catch {
@@ -1035,7 +1345,7 @@ var ApiTelemetryClient = class {
1035
1345
  var ConsoleTelemetryClient = class {
1036
1346
  async record(event) {
1037
1347
  const serialized = JSON.stringify(event, null, 2);
1038
- process3.stdout.write(`TELEMETRY EVENT: ${serialized}
1348
+ process4.stdout.write(`TELEMETRY EVENT: ${serialized}
1039
1349
  `);
1040
1350
  }
1041
1351
  };
@@ -1062,11 +1372,11 @@ var TelemetryService = class {
1062
1372
  if (override) {
1063
1373
  return override === "1";
1064
1374
  }
1065
- if (process3__default.default.env.CI || process3__default.default.env.DO_NOT_TRACK) {
1375
+ if (process4__default.default.env.CI || process4__default.default.env.DO_NOT_TRACK) {
1066
1376
  return false;
1067
1377
  }
1068
- if (process3__default.default.env.SPG_TELEMETRY) {
1069
- return process3__default.default.env.SPG_TELEMETRY === "1";
1378
+ if (process4__default.default.env.SPG_TELEMETRY) {
1379
+ return process4__default.default.env.SPG_TELEMETRY === "1";
1070
1380
  }
1071
1381
  const stored = this.getStoredPreference();
1072
1382
  if (stored === void 0) {
@@ -1108,7 +1418,7 @@ var TelemetryService = class {
1108
1418
  durationMs: now - startedAt,
1109
1419
  packageName: this.packageName,
1110
1420
  packageVersion: this.packageVersion,
1111
- nodeVersion: process3__default.default.version,
1421
+ nodeVersion: process4__default.default.version,
1112
1422
  osPlatform: os__default.default.platform(),
1113
1423
  osRelease: os__default.default.release(),
1114
1424
  osArch: os__default.default.arch(),
@@ -1142,7 +1452,7 @@ var TelemetryService = class {
1142
1452
  /** Returns the telemetry client. */
1143
1453
  getClient() {
1144
1454
  if (!this.client) {
1145
- switch (process3__default.default.env.SPG_TELEMETRY_PROVIDER) {
1455
+ switch (process4__default.default.env.SPG_TELEMETRY_PROVIDER) {
1146
1456
  case "none": {
1147
1457
  this.client = void 0;
1148
1458
  break;
@@ -1234,7 +1544,7 @@ async function waitForUpdateCheck(checkPromise) {
1234
1544
  // package.json
1235
1545
  var package_default = {
1236
1546
  name: "simple-photo-gallery",
1237
- version: "2.0.18"};
1547
+ version: "2.1.0"};
1238
1548
 
1239
1549
  // src/index.ts
1240
1550
  var program = new commander.Command();
@@ -1276,7 +1586,7 @@ function withCommandContext(handler) {
1276
1586
  } catch (error) {
1277
1587
  ui.debug(error);
1278
1588
  errorInfo = error instanceof Error ? { name: error.name, message: error.message } : { name: "UnknownError", message: String(error) };
1279
- process3__default.default.exitCode = 1;
1589
+ process4__default.default.exitCode = 1;
1280
1590
  }
1281
1591
  const updateInfo = await waitForUpdateCheck(updateCheckPromise);
1282
1592
  if (updateInfo) {
@@ -1298,14 +1608,21 @@ function withCommandContext(handler) {
1298
1608
  program.command("init").description("Initialize a gallery by scaning a folder for images and videos").option(
1299
1609
  "-p, --photos <path>",
1300
1610
  "Path to the folder where the photos are stored. Default: current working directory",
1301
- process3__default.default.cwd()
1611
+ process4__default.default.cwd()
1302
1612
  ).option(
1303
1613
  "-g, --gallery <path>",
1304
1614
  "Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
1305
- ).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).option("--cta-banner", "Add a Simple Photo Gallery call-to-action banner to the end of the gallery", false).action(withCommandContext((options, ui) => init(options, ui)));
1306
- 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__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).action(withCommandContext((options, ui) => thumbnails(options, ui)));
1307
- 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__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").option("-t, --thumbs-base-url <url>", "Base URL where the thumbnails are hosted").option("--no-thumbnails", "Skip creating thumbnails when building the gallery", true).option("--no-scan", "Do not scan for new photos when building the gallery", true).action(withCommandContext((options, ui) => build(options, ui)));
1308
- 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__default.default.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
1615
+ ).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).option("--cta-banner", "Add a Simple Photo Gallery call-to-action banner to the end of the gallery", false).option("--theme <package|path>", "Theme package name or local path to store in gallery.json").option("--thumbnail-size <pixels>", "Thumbnail size in pixels to store in gallery.json", Number.parseInt).option("--thumbnail-edge <mode>", "How thumbnail size is applied: auto, width, or height").action(withCommandContext((options, ui) => init(options, ui)));
1616
+ 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", process4__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("--thumbnail-size <pixels>", "Override thumbnail size in pixels", Number.parseInt).option("--thumbnail-edge <mode>", "Override how thumbnail size is applied: auto, width, or height").action(withCommandContext((options, ui) => thumbnails(options, ui)));
1617
+ 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", process4__default.default.cwd()).option("-r, --recursive", "Scan subdirectories recursively", false).option("-b, --base-url <url>", "Base URL where the photos are hosted").option("-t, --thumbs-base-url <url>", "Base URL where the thumbnails are hosted").option("--no-thumbnails", "Skip creating thumbnails when building the gallery", true).option("--no-scan", "Do not scan for new photos when building the gallery", true).option(
1618
+ "--theme <package|path>",
1619
+ "Theme package name (e.g., @simple-photo-gallery/theme-modern) or local path (e.g., ./themes/my-theme)"
1620
+ ).option("--thumbnail-size <pixels>", "Override thumbnail size in pixels", Number.parseInt).option("--thumbnail-edge <mode>", "Override how thumbnail size is applied: auto, width, or height").action(withCommandContext((options, ui) => build(options, ui)));
1621
+ 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", process4__default.default.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
1622
+ program.command("create-theme").description("Create a new theme template").argument("<name>", "Name of the theme to create").option("-p, --path <path>", "Path where the theme should be created. Default: ./themes/<name>").action(async (name, options, command) => {
1623
+ const handler = withCommandContext((opts, ui) => createTheme({ name, path: opts.path }, ui));
1624
+ await handler(options, command);
1625
+ });
1309
1626
  program.command("telemetry").description("Manage anonymous telemetry preferences. Use 1 to enable, 0 to disable, or no argument to check status").option("-s, --state <state>", "Enable (1) or disable (0) telemetry", parseTelemetryOption).action(withCommandContext((options, ui) => telemetry(options, ui, telemetryService)));
1310
1627
  program.parse();
1311
1628
  //# sourceMappingURL=index.cjs.map