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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import process3, { stdout } from 'process';
2
+ import process4, { stdout } from 'process';
3
3
  import { Command } from 'commander';
4
4
  import { LogLevels, createConsola } from 'consola';
5
5
  import { execSync, spawn } from 'child_process';
@@ -9,15 +9,16 @@ import { Buffer } from 'buffer';
9
9
  import sharp2 from 'sharp';
10
10
  import { encode } from 'blurhash';
11
11
  import { GalleryDataSchema, GalleryDataDeprecatedSchema } from '@simple-photo-gallery/common';
12
+ import { extractThumbnailConfigFromGallery, loadThemeConfig, mergeThumbnailConfig } from '@simple-photo-gallery/common/theme';
12
13
  import ExifReader from 'exifreader';
13
14
  import ffprobe from 'node-ffprobe';
15
+ import { fileURLToPath } from 'url';
14
16
  import os from 'os';
15
17
  import Conf from 'conf';
16
18
  import axios from 'axios';
17
19
  import { compareSemVer, parseSemVer } from 'semver-parser';
18
20
 
19
21
  // src/config/index.ts
20
- var DEFAULT_THUMBNAIL_SIZE = 300;
21
22
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".tiff", ".tif", ".svg", ".avif"]);
22
23
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".avi", ".mov", ".wmv", ".flv", ".webm", ".mkv", ".m4v", ".3gp"]);
23
24
  var HEADER_IMAGE_LANDSCAPE_WIDTHS = [3840, 2560, 1920, 1280, 960, 640];
@@ -46,7 +47,7 @@ async function cropAndResizeImage(image, outputPath, width, height, format = "av
46
47
  withoutEnlargement: true
47
48
  }).toFormat(format).toFile(outputPath);
48
49
  }
49
- async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size) {
50
+ async function createImageThumbnails(image, metadata, outputPath, outputPathRetina, size, sizeDimension = "auto") {
50
51
  const originalWidth = metadata.width || 0;
51
52
  const originalHeight = metadata.height || 0;
52
53
  if (originalWidth === 0 || originalHeight === 0) {
@@ -55,12 +56,20 @@ async function createImageThumbnails(image, metadata, outputPath, outputPathReti
55
56
  const aspectRatio = originalWidth / originalHeight;
56
57
  let width;
57
58
  let height;
58
- if (originalWidth > originalHeight) {
59
+ if (sizeDimension === "width") {
59
60
  width = size;
60
61
  height = Math.round(size / aspectRatio);
61
- } else {
62
+ } else if (sizeDimension === "height") {
62
63
  width = Math.round(size * aspectRatio);
63
64
  height = size;
65
+ } else {
66
+ if (originalWidth > originalHeight) {
67
+ width = size;
68
+ height = Math.round(size / aspectRatio);
69
+ } else {
70
+ width = Math.round(size * aspectRatio);
71
+ height = size;
72
+ }
64
73
  }
65
74
  await resizeImage(image, outputPath, width, height);
66
75
  await resizeImage(image, outputPathRetina, width * 2, height * 2);
@@ -326,11 +335,15 @@ function migrateGalleryJson(deprecatedGalleryData, galleryJsonPath, ui) {
326
335
  filename: path7.basename(image.path)
327
336
  }))
328
337
  }));
338
+ const thumbnails2 = deprecatedGalleryData.thumbnailSize === void 0 ? void 0 : { size: deprecatedGalleryData.thumbnailSize };
329
339
  const galleryData = {
330
340
  ...deprecatedGalleryData,
341
+ thumbnailSize: void 0,
342
+ // Remove old field
331
343
  headerImage: path7.basename(deprecatedGalleryData.headerImage),
332
344
  sections,
333
- mediaBasePath
345
+ mediaBasePath,
346
+ thumbnails: thumbnails2
334
347
  };
335
348
  ui.debug("Backing up old gallery.json file");
336
349
  fs8.copyFileSync(galleryJsonPath, `${galleryJsonPath}.old`);
@@ -401,9 +414,18 @@ async function getGallerySettingsFromUser(galleryName, defaultImage, ui) {
401
414
  default: defaultImage,
402
415
  placeholder: defaultImage
403
416
  });
404
- return { title, description, url, headerImage };
417
+ return {
418
+ title,
419
+ description,
420
+ url,
421
+ headerImage,
422
+ thumbnails: {
423
+ size: "",
424
+ edge: void 0
425
+ }
426
+ };
405
427
  }
406
- async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ctaBanner, ui) {
428
+ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries = [], useDefaultSettings, ctaBanner, configOptions, ui) {
407
429
  const galleryDir = path7.dirname(galleryJsonPath);
408
430
  const isSameLocation = path7.relative(scanPath, path7.join(galleryDir, "..")) === "";
409
431
  const mediaBasePath = isSameLocation ? void 0 : scanPath;
@@ -411,12 +433,23 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalle
411
433
  ...subGallery,
412
434
  headerImage: subGallery.headerImage ? path7.relative(galleryDir, subGallery.headerImage) : ""
413
435
  }));
436
+ const thumbnailsConfig = {};
437
+ if (configOptions.thumbnailSize !== void 0) {
438
+ thumbnailsConfig.size = configOptions.thumbnailSize;
439
+ }
440
+ if (configOptions.thumbnailEdge !== void 0) {
441
+ thumbnailsConfig.edge = configOptions.thumbnailEdge;
442
+ }
414
443
  let galleryData = {
415
444
  title: "My Gallery",
416
445
  description: "My gallery with fantastic photos.",
417
446
  headerImage: mediaFiles[0]?.filename || "",
418
447
  mediaBasePath,
419
448
  metadata: {},
449
+ // Include theme if provided via CLI
450
+ ...configOptions.theme && { theme: configOptions.theme },
451
+ // Include thumbnails if any values were set via CLI
452
+ ...Object.keys(thumbnailsConfig).length > 0 && { thumbnails: thumbnailsConfig },
420
453
  sections: [
421
454
  {
422
455
  images: mediaFiles
@@ -429,13 +462,15 @@ async function createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalle
429
462
  ...ctaBanner !== void 0 && { ctaBanner }
430
463
  };
431
464
  if (!useDefaultSettings) {
465
+ const userSettings = await getGallerySettingsFromUser(
466
+ path7.basename(path7.join(galleryDir, "..")),
467
+ path7.basename(mediaFiles[0]?.filename || ""),
468
+ ui
469
+ );
470
+ const { thumbnails: thumbnails2, ...otherSettings } = userSettings;
432
471
  galleryData = {
433
472
  ...galleryData,
434
- ...await getGallerySettingsFromUser(
435
- path7.basename(path7.join(galleryDir, "..")),
436
- path7.basename(mediaFiles[0]?.filename || ""),
437
- ui
438
- )
473
+ ...otherSettings
439
474
  };
440
475
  }
441
476
  await promises.writeFile(galleryJsonPath, JSON.stringify(galleryData, null, 2));
@@ -450,7 +485,7 @@ async function galleryExists(outputPath) {
450
485
  return false;
451
486
  }
452
487
  }
453
- async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ctaBanner, ui) {
488
+ async function processDirectory(scanPath, outputPath, recursive, useDefaultSettings, force, ctaBanner, configOptions, ui) {
454
489
  ui.start(`Scanning ${scanPath}`);
455
490
  let totalFiles = 0;
456
491
  let totalGalleries = 1;
@@ -466,6 +501,7 @@ async function processDirectory(scanPath, outputPath, recursive, useDefaultSetti
466
501
  useDefaultSettings,
467
502
  force,
468
503
  ctaBanner,
504
+ configOptions,
469
505
  ui
470
506
  );
471
507
  totalFiles += result2.totalFiles;
@@ -491,7 +527,16 @@ async function processDirectory(scanPath, outputPath, recursive, useDefaultSetti
491
527
  }
492
528
  try {
493
529
  await promises.mkdir(galleryPath, { recursive: true });
494
- await createGalleryJson(mediaFiles, galleryJsonPath, scanPath, subGalleries, useDefaultSettings, ctaBanner, ui);
530
+ await createGalleryJson(
531
+ mediaFiles,
532
+ galleryJsonPath,
533
+ scanPath,
534
+ subGalleries,
535
+ useDefaultSettings,
536
+ ctaBanner,
537
+ configOptions,
538
+ ui
539
+ );
495
540
  ui.success(
496
541
  `Create gallery with ${mediaFiles.length} files and ${subGalleries.length} subgalleries at: ${galleryJsonPath}`
497
542
  );
@@ -515,6 +560,11 @@ async function init(options, ui) {
515
560
  try {
516
561
  const scanPath = path7.resolve(options.photos);
517
562
  const outputPath = options.gallery ? path7.resolve(options.gallery) : scanPath;
563
+ const configOptions = {
564
+ theme: options.theme,
565
+ thumbnailSize: options.thumbnailSize,
566
+ thumbnailEdge: options.thumbnailEdge
567
+ };
518
568
  const result = await processDirectory(
519
569
  scanPath,
520
570
  outputPath,
@@ -522,6 +572,7 @@ async function init(options, ui) {
522
572
  options.default,
523
573
  options.force,
524
574
  options.ctaBanner,
575
+ configOptions,
525
576
  ui
526
577
  );
527
578
  ui.box(
@@ -571,9 +622,25 @@ async function getVideoDimensions(filePath) {
571
622
  }
572
623
  return dimensions;
573
624
  }
574
- async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, height, verbose = false) {
625
+ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, outputPathRetina, size, sizeDimension = "auto", verbose = false) {
575
626
  const aspectRatio = videoDimensions.width / videoDimensions.height;
576
- const width = Math.round(height * aspectRatio);
627
+ let width;
628
+ let height;
629
+ if (sizeDimension === "width") {
630
+ width = size;
631
+ height = Math.round(size / aspectRatio);
632
+ } else if (sizeDimension === "height") {
633
+ width = Math.round(size * aspectRatio);
634
+ height = size;
635
+ } else {
636
+ if (videoDimensions.width > videoDimensions.height) {
637
+ width = size;
638
+ height = Math.round(size / aspectRatio);
639
+ } else {
640
+ width = Math.round(size * aspectRatio);
641
+ height = size;
642
+ }
643
+ }
577
644
  const tempFramePath = `${outputPath}.temp.png`;
578
645
  return new Promise((resolve, reject) => {
579
646
  const ffmpeg = spawn("ffmpeg", [
@@ -614,7 +681,7 @@ async function createVideoThumbnails(inputPath, videoDimensions, outputPath, out
614
681
  }
615
682
 
616
683
  // src/modules/thumbnails/index.ts
617
- async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) {
684
+ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, thumbnailSizeDimension = "auto", lastMediaTimestamp) {
618
685
  const fileMtime = await getFileMtime(imagePath);
619
686
  if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8.existsSync(thumbnailPath)) {
620
687
  return void 0;
@@ -633,7 +700,8 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
633
700
  metadata,
634
701
  thumbnailPath,
635
702
  thumbnailPathRetina,
636
- thumbnailSize
703
+ thumbnailSize,
704
+ thumbnailSizeDimension
637
705
  );
638
706
  const blurHash = await generateBlurHash(thumbnailPath);
639
707
  return {
@@ -652,7 +720,7 @@ async function processImage(imagePath, thumbnailPath, thumbnailPathRetina, thumb
652
720
  lastMediaTimestamp: fileMtime.toISOString()
653
721
  };
654
722
  }
655
- async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp) {
723
+ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumbnailSize, thumbnailSizeDimension = "auto", verbose, lastMediaTimestamp) {
656
724
  const fileMtime = await getFileMtime(videoPath);
657
725
  if (lastMediaTimestamp && fileMtime <= lastMediaTimestamp && fs8.existsSync(thumbnailPath)) {
658
726
  return void 0;
@@ -664,6 +732,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
664
732
  thumbnailPath,
665
733
  thumbnailPathRetina,
666
734
  thumbnailSize,
735
+ thumbnailSizeDimension,
667
736
  verbose
668
737
  );
669
738
  const blurHash = await generateBlurHash(thumbnailPath);
@@ -683,7 +752,7 @@ async function processVideo(videoPath, thumbnailPath, thumbnailPathRetina, thumb
683
752
  lastMediaTimestamp: fileMtime.toISOString()
684
753
  };
685
754
  }
686
- async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailSize, ui) {
755
+ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailConfig, ui) {
687
756
  try {
688
757
  const filePath = path7.resolve(path7.join(mediaBasePath, mediaFile.filename));
689
758
  const fileName = mediaFile.filename;
@@ -694,7 +763,22 @@ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbn
694
763
  const lastMediaTimestamp = mediaFile.lastMediaTimestamp ? new Date(mediaFile.lastMediaTimestamp) : void 0;
695
764
  const verbose = ui.level === LogLevels.debug;
696
765
  ui.debug(` Processing ${mediaFile.type}: ${fileName}`);
697
- const updatedMediaFile = await (mediaFile.type === "image" ? processImage(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, lastMediaTimestamp) : processVideo(filePath, thumbnailPath, thumbnailPathRetina, thumbnailSize, verbose, lastMediaTimestamp));
766
+ const updatedMediaFile = await (mediaFile.type === "image" ? processImage(
767
+ filePath,
768
+ thumbnailPath,
769
+ thumbnailPathRetina,
770
+ thumbnailConfig.size,
771
+ thumbnailConfig.edge,
772
+ lastMediaTimestamp
773
+ ) : processVideo(
774
+ filePath,
775
+ thumbnailPath,
776
+ thumbnailPathRetina,
777
+ thumbnailConfig.size,
778
+ thumbnailConfig.edge,
779
+ verbose,
780
+ lastMediaTimestamp
781
+ ));
698
782
  if (!updatedMediaFile) {
699
783
  ui.debug(` Skipping ${fileName} because it has already been processed`);
700
784
  if (mediaFile.thumbnail && !mediaFile.thumbnail.blurHash && fs8.existsSync(thumbnailPath)) {
@@ -730,19 +814,30 @@ async function processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbn
730
814
  return { ...mediaFile, thumbnail: void 0 };
731
815
  }
732
816
  }
733
- async function processGalleryThumbnails(galleryDir, ui) {
817
+ async function processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig) {
734
818
  const galleryJsonPath = path7.join(galleryDir, "gallery", "gallery.json");
735
819
  const thumbnailsPath = path7.join(galleryDir, "gallery", "images");
736
820
  ui.start(`Creating thumbnails: ${galleryDir}`);
737
821
  try {
738
822
  fs8.mkdirSync(thumbnailsPath, { recursive: true });
739
823
  const galleryData = parseGalleryJson(galleryJsonPath, ui);
740
- const thumbnailSize = galleryData.thumbnailSize || DEFAULT_THUMBNAIL_SIZE;
824
+ const galleryThumbnailConfig = extractThumbnailConfigFromGallery(galleryData);
825
+ let themeConfig;
826
+ if (galleryData.theme) {
827
+ try {
828
+ const themeDir = await resolveThemeDir(galleryData.theme, ui);
829
+ themeConfig = loadThemeConfig(themeDir);
830
+ } catch {
831
+ ui.debug(`Could not load theme config from ${galleryData.theme}, using defaults`);
832
+ }
833
+ }
834
+ const thumbnailConfig = mergeThumbnailConfig(cliThumbnailConfig, galleryThumbnailConfig, themeConfig);
835
+ ui.debug(`Thumbnail config: size=${thumbnailConfig.size}, edge=${thumbnailConfig.edge}`);
741
836
  const mediaBasePath = galleryData.mediaBasePath ?? path7.join(galleryDir);
742
837
  let processedCount = 0;
743
838
  for (const section of galleryData.sections) {
744
839
  for (const [index, mediaFile] of section.images.entries()) {
745
- section.images[index] = await processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailSize, ui);
840
+ section.images[index] = await processMediaFile(mediaFile, mediaBasePath, thumbnailsPath, thumbnailConfig, ui);
746
841
  }
747
842
  processedCount += section.images.length;
748
843
  }
@@ -761,10 +856,11 @@ async function thumbnails(options, ui) {
761
856
  ui.error("No galleries found.");
762
857
  return { processedGalleryCount: 0, processedMediaCount: 0 };
763
858
  }
859
+ const cliThumbnailConfig = options.thumbnailSize !== void 0 || options.thumbnailEdge !== void 0 ? { size: options.thumbnailSize, edge: options.thumbnailEdge } : void 0;
764
860
  let totalGalleries = 0;
765
861
  let totalProcessed = 0;
766
862
  for (const galleryDir of galleryDirs) {
767
- const processed = await processGalleryThumbnails(galleryDir, ui);
863
+ const processed = await processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig);
768
864
  if (processed > 0) {
769
865
  ++totalGalleries;
770
866
  totalProcessed += processed;
@@ -823,7 +919,7 @@ async function scanAndAppendNewFiles(galleryDir, galleryJsonPath, galleryData, u
823
919
  }
824
920
  return galleryData;
825
921
  }
826
- async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl) {
922
+ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnails, ui, baseUrl, thumbsBaseUrl, cliThumbnailConfig, cliTheme) {
827
923
  ui.start(`Building gallery ${galleryDir}`);
828
924
  const galleryJsonPath = path7.join(galleryDir, "gallery", "gallery.json");
829
925
  let galleryData = parseGalleryJson(galleryJsonPath, ui);
@@ -876,18 +972,35 @@ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnail
876
972
  galleryData.thumbsBaseUrl = thumbsBaseUrl;
877
973
  fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
878
974
  }
975
+ if (cliTheme && galleryData.theme !== cliTheme) {
976
+ ui.debug("Updating gallery.json with theme");
977
+ galleryData.theme = cliTheme;
978
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
979
+ }
980
+ if (cliThumbnailConfig) {
981
+ const needsUpdate = cliThumbnailConfig.size !== void 0 && galleryData.thumbnails?.size !== cliThumbnailConfig.size || cliThumbnailConfig.edge !== void 0 && galleryData.thumbnails?.edge !== cliThumbnailConfig.edge;
982
+ if (needsUpdate) {
983
+ ui.debug("Updating gallery.json with thumbnail settings");
984
+ galleryData.thumbnails = {
985
+ ...galleryData.thumbnails,
986
+ ...cliThumbnailConfig.size !== void 0 && { size: cliThumbnailConfig.size },
987
+ ...cliThumbnailConfig.edge !== void 0 && { edge: cliThumbnailConfig.edge }
988
+ };
989
+ fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
990
+ }
991
+ }
879
992
  if (!galleryData.metadata.image) {
880
993
  ui.debug("Updating gallery.json with social media card URL");
881
994
  galleryData.metadata.image = thumbsBaseUrl ? `${thumbsBaseUrl}/${path7.basename(socialMediaCardImagePath)}` : `${galleryData.url || ""}/${path7.relative(galleryDir, socialMediaCardImagePath)}`;
882
995
  fs8.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
883
996
  }
884
997
  if (shouldCreateThumbnails) {
885
- await processGalleryThumbnails(galleryDir, ui);
998
+ await processGalleryThumbnails(galleryDir, ui, cliThumbnailConfig);
886
999
  }
887
1000
  ui.debug("Building gallery from template");
888
1001
  try {
889
- process3.env.GALLERY_JSON_PATH = galleryJsonPath;
890
- process3.env.GALLERY_OUTPUT_DIR = path7.join(galleryDir, "gallery");
1002
+ process4.env.GALLERY_JSON_PATH = galleryJsonPath;
1003
+ process4.env.GALLERY_OUTPUT_DIR = path7.join(galleryDir, "gallery");
891
1004
  execSync("npx astro build", { cwd: templateDir, stdio: ui.level === LogLevels.debug ? "inherit" : "ignore" });
892
1005
  } catch (error) {
893
1006
  ui.error(`Build failed for ${galleryDir}`);
@@ -904,6 +1017,25 @@ async function buildGallery(galleryDir, templateDir, scan, shouldCreateThumbnail
904
1017
  fs8.rmSync(buildDir, { recursive: true, force: true });
905
1018
  ui.success(`Gallery built successfully`);
906
1019
  }
1020
+ function isLocalThemePath(theme) {
1021
+ return theme.startsWith("./") || theme.startsWith("../") || theme.startsWith("/");
1022
+ }
1023
+ async function resolveThemeDir(theme, ui) {
1024
+ if (isLocalThemePath(theme)) {
1025
+ const themeDir = path7.resolve(theme);
1026
+ const packageJsonPath = path7.join(themeDir, "package.json");
1027
+ if (!fs8.existsSync(packageJsonPath)) {
1028
+ throw new Error(`Theme directory not found or invalid: ${themeDir}. package.json not found.`);
1029
+ }
1030
+ ui.debug(`Using local theme: ${themeDir}`);
1031
+ return themeDir;
1032
+ } else {
1033
+ const themePath = await import.meta.resolve(`${theme}/package.json`);
1034
+ const themeDir = path7.dirname(new URL(themePath).pathname);
1035
+ ui.debug(`Using npm theme package: ${theme} (${themeDir})`);
1036
+ return themeDir;
1037
+ }
1038
+ }
907
1039
  async function build(options, ui) {
908
1040
  try {
909
1041
  const galleryDirs = findGalleries(options.gallery, options.recursive);
@@ -911,20 +1043,41 @@ async function build(options, ui) {
911
1043
  ui.error("No galleries found.");
912
1044
  return { processedGalleryCount: 0 };
913
1045
  }
914
- const themePath = await import.meta.resolve("@simple-photo-gallery/theme-modern/package.json");
915
- const themeDir = path7.dirname(new URL(themePath).pathname);
1046
+ const cliThumbnailConfig = options.thumbnailSize !== void 0 || options.thumbnailEdge !== void 0 ? { size: options.thumbnailSize, edge: options.thumbnailEdge } : void 0;
916
1047
  let totalGalleries = 0;
917
1048
  for (const dir of galleryDirs) {
1049
+ const galleryJsonPath = path7.join(dir, "gallery", "gallery.json");
1050
+ const galleryData = parseGalleryJson(galleryJsonPath, ui);
1051
+ const themeIdentifier = options.theme || galleryData.theme || "@simple-photo-gallery/theme-modern";
1052
+ const themeDir = await resolveThemeDir(themeIdentifier, ui);
918
1053
  const baseUrl = options.baseUrl ? `${options.baseUrl}${path7.relative(options.gallery, dir)}` : void 0;
919
1054
  const thumbsBaseUrl = options.thumbsBaseUrl ? `${options.thumbsBaseUrl}${path7.relative(options.gallery, dir)}` : void 0;
920
- await buildGallery(path7.resolve(dir), themeDir, options.scan, options.thumbnails, ui, baseUrl, thumbsBaseUrl);
1055
+ await buildGallery(
1056
+ path7.resolve(dir),
1057
+ themeDir,
1058
+ options.scan,
1059
+ options.thumbnails,
1060
+ ui,
1061
+ baseUrl,
1062
+ thumbsBaseUrl,
1063
+ cliThumbnailConfig,
1064
+ options.theme
1065
+ );
921
1066
  ++totalGalleries;
922
1067
  }
923
1068
  ui.box(`Built ${totalGalleries} ${totalGalleries === 1 ? "gallery" : "galleries"} successfully`);
924
1069
  return { processedGalleryCount: totalGalleries };
925
1070
  } catch (error) {
926
- if (error instanceof Error && error.message.includes("Cannot find package")) {
927
- ui.error("Theme package not found: @simple-photo-gallery/theme-modern/package.json");
1071
+ if (error instanceof Error) {
1072
+ if (error.message.includes("Cannot find package")) {
1073
+ ui.error(
1074
+ `Theme package not found: ${options.theme || "@simple-photo-gallery/theme-modern"}. Make sure it's installed.`
1075
+ );
1076
+ } else if (error.message.includes("Theme directory not found") || error.message.includes("package.json not found")) {
1077
+ ui.error(error.message);
1078
+ } else {
1079
+ ui.error("Error building gallery");
1080
+ }
928
1081
  } else {
929
1082
  ui.error("Error building gallery");
930
1083
  }
@@ -982,6 +1135,162 @@ async function clean(options, ui) {
982
1135
  throw error;
983
1136
  }
984
1137
  }
1138
+ function findMonorepoRoot(startDir) {
1139
+ let dir = path7.resolve(startDir);
1140
+ while (true) {
1141
+ const pkgPath = path7.join(dir, "package.json");
1142
+ if (fs8.existsSync(pkgPath)) {
1143
+ try {
1144
+ const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf8"));
1145
+ if (pkg && typeof pkg === "object" && "workspaces" in pkg) {
1146
+ return dir;
1147
+ }
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ const parent = path7.dirname(dir);
1152
+ if (parent === dir) {
1153
+ return void 0;
1154
+ }
1155
+ dir = parent;
1156
+ }
1157
+ }
1158
+ function validateThemeName(name) {
1159
+ if (!name || name.trim().length === 0) {
1160
+ throw new Error("Theme name cannot be empty");
1161
+ }
1162
+ if (!/^[a-z0-9-]+$/i.test(name)) {
1163
+ throw new Error("Theme name can only contain letters, numbers, and hyphens");
1164
+ }
1165
+ return true;
1166
+ }
1167
+ async function ensureDirectory(dirPath, ui) {
1168
+ try {
1169
+ await fs8.promises.mkdir(dirPath, { recursive: true });
1170
+ ui.debug(`Created directory: ${dirPath}`);
1171
+ } catch (error) {
1172
+ if (error instanceof Error && "code" in error && error.code !== "EEXIST") {
1173
+ throw new Error(`Failed to create directory ${dirPath}: ${error.message}`);
1174
+ }
1175
+ }
1176
+ }
1177
+ var EXCLUDE_PATTERNS = ["node_modules", ".astro", "dist", "_build", ".git", "*.log", ".DS_Store"];
1178
+ function shouldExclude(name) {
1179
+ if (name === "README.md" || name === "README_BASE.md") {
1180
+ return true;
1181
+ }
1182
+ return EXCLUDE_PATTERNS.some((pattern) => {
1183
+ if (pattern.includes("*")) {
1184
+ const regexPattern = pattern.split("*").join(".*");
1185
+ const regex = new RegExp(regexPattern);
1186
+ return regex.test(name);
1187
+ }
1188
+ return name === pattern;
1189
+ });
1190
+ }
1191
+ async function copyDirectory(src, dest, ui) {
1192
+ await fs8.promises.mkdir(dest, { recursive: true });
1193
+ const entries = await fs8.promises.readdir(src, { withFileTypes: true });
1194
+ for (const entry of entries) {
1195
+ if (shouldExclude(entry.name)) {
1196
+ ui.debug(`Skipping excluded file/directory: ${entry.name}`);
1197
+ continue;
1198
+ }
1199
+ const srcPath = path7.join(src, entry.name);
1200
+ const destPath = path7.join(dest, entry.name);
1201
+ if (entry.isDirectory()) {
1202
+ await copyDirectory(srcPath, destPath, ui);
1203
+ } else {
1204
+ await fs8.promises.copyFile(srcPath, destPath);
1205
+ ui.debug(`Copied file: ${destPath}`);
1206
+ }
1207
+ }
1208
+ }
1209
+ function findBaseThemePath() {
1210
+ const moduleDir = path7.dirname(fileURLToPath(import.meta.url));
1211
+ const bundledTemplatePath = path7.resolve(moduleDir, "../../../src/modules/create-theme/templates/base");
1212
+ if (fs8.existsSync(bundledTemplatePath)) {
1213
+ return bundledTemplatePath;
1214
+ }
1215
+ const monorepoRoot = findMonorepoRoot(process4.cwd());
1216
+ const workspaceRoot = monorepoRoot ?? process4.cwd();
1217
+ const workspaceBaseThemePath = path7.join(workspaceRoot, "themes", "base");
1218
+ if (fs8.existsSync(workspaceBaseThemePath)) {
1219
+ return workspaceBaseThemePath;
1220
+ }
1221
+ throw new Error(
1222
+ `Base theme template not found. Tried:
1223
+ - ${bundledTemplatePath}
1224
+ - ${workspaceBaseThemePath}
1225
+
1226
+ Please ensure the templates are included in the package or themes/base exists in the workspace.`
1227
+ );
1228
+ }
1229
+ async function updatePackageJson(themeDir, themeName, ui) {
1230
+ const packageJsonPath = path7.join(themeDir, "package.json");
1231
+ const packageJsonContent = await fs8.promises.readFile(packageJsonPath, "utf8");
1232
+ const packageJson = JSON.parse(packageJsonContent);
1233
+ packageJson.name = themeName;
1234
+ await fs8.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
1235
+ ui.debug(`Updated package.json with theme name: ${themeName}`);
1236
+ }
1237
+ async function createReadmeFromBase(baseThemePath, themeDir, themeName, ui) {
1238
+ const readmeBasePath = path7.join(baseThemePath, "README_BASE.md");
1239
+ const readmePath = path7.join(themeDir, "README.md");
1240
+ if (!fs8.existsSync(readmeBasePath)) {
1241
+ throw new Error(`README_BASE.md not found in template: ${readmeBasePath}`);
1242
+ }
1243
+ let readme = await fs8.promises.readFile(readmeBasePath, "utf8");
1244
+ const displayName = themeName.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1245
+ readme = readme.replaceAll("{THEME_NAME}", displayName);
1246
+ readme = readme.replaceAll("{THEME_NAME_LOWER}", displayName.toLowerCase());
1247
+ await fs8.promises.writeFile(readmePath, readme, "utf8");
1248
+ ui.debug(`Created README.md from README_BASE.md for theme: ${themeName}`);
1249
+ }
1250
+ async function createTheme(options, ui) {
1251
+ try {
1252
+ validateThemeName(options.name);
1253
+ let themeDir;
1254
+ if (options.path) {
1255
+ themeDir = path7.resolve(options.path);
1256
+ } else {
1257
+ const monorepoRoot = findMonorepoRoot(process4.cwd());
1258
+ const baseDir = monorepoRoot ?? process4.cwd();
1259
+ const themesBaseDir = path7.resolve(baseDir, "themes");
1260
+ themeDir = path7.join(themesBaseDir, options.name);
1261
+ if (!fs8.existsSync(themesBaseDir)) {
1262
+ await ensureDirectory(themesBaseDir, ui);
1263
+ }
1264
+ }
1265
+ if (fs8.existsSync(themeDir)) {
1266
+ throw new Error(`Theme directory already exists: ${themeDir}. Cannot overwrite existing theme.`);
1267
+ }
1268
+ ui.start(`Creating theme: ${options.name}`);
1269
+ const baseThemePath = findBaseThemePath();
1270
+ ui.debug(`Using base theme from: ${baseThemePath}`);
1271
+ ui.debug("Copying base theme files...");
1272
+ await copyDirectory(baseThemePath, themeDir, ui);
1273
+ ui.debug("Updating theme-specific files...");
1274
+ await updatePackageJson(themeDir, options.name, ui);
1275
+ await createReadmeFromBase(baseThemePath, themeDir, options.name, ui);
1276
+ ui.success(`Theme created successfully at: ${themeDir}`);
1277
+ ui.info(`
1278
+ Next steps:`);
1279
+ ui.info(`1. cd ${themeDir}`);
1280
+ ui.info(`2. yarn install`);
1281
+ ui.info(`3. Customize your theme in src/pages/index.astro`);
1282
+ ui.info(`4. Initialize a gallery (run from directory with your images): spg init -p <images-folder>`);
1283
+ ui.info(`5. Build a gallery with your theme: spg build --theme ${themeDir} -g <gallery-folder>`);
1284
+ return { processedGalleryCount: 0 };
1285
+ } catch (error) {
1286
+ if (error instanceof Error) {
1287
+ ui.error(error.message);
1288
+ } else {
1289
+ ui.error("Failed to create theme");
1290
+ }
1291
+ throw error;
1292
+ }
1293
+ }
985
1294
 
986
1295
  // src/modules/telemetry/index.ts
987
1296
  async function telemetry(options, ui, telemetryService2) {
@@ -1011,7 +1320,7 @@ var ApiTelemetryClient = class {
1011
1320
  axios.post(this.endpoint, event, {
1012
1321
  headers: {
1013
1322
  "content-type": "application/json",
1014
- "user-agent": `simple-photo-gallery/${event.packageVersion} (${process3.platform}; ${process3.arch})`
1323
+ "user-agent": `simple-photo-gallery/${event.packageVersion} (${process4.platform}; ${process4.arch})`
1015
1324
  }
1016
1325
  });
1017
1326
  } catch {
@@ -1048,11 +1357,11 @@ var TelemetryService = class {
1048
1357
  if (override) {
1049
1358
  return override === "1";
1050
1359
  }
1051
- if (process3.env.CI || process3.env.DO_NOT_TRACK) {
1360
+ if (process4.env.CI || process4.env.DO_NOT_TRACK) {
1052
1361
  return false;
1053
1362
  }
1054
- if (process3.env.SPG_TELEMETRY) {
1055
- return process3.env.SPG_TELEMETRY === "1";
1363
+ if (process4.env.SPG_TELEMETRY) {
1364
+ return process4.env.SPG_TELEMETRY === "1";
1056
1365
  }
1057
1366
  const stored = this.getStoredPreference();
1058
1367
  if (stored === void 0) {
@@ -1094,7 +1403,7 @@ var TelemetryService = class {
1094
1403
  durationMs: now - startedAt,
1095
1404
  packageName: this.packageName,
1096
1405
  packageVersion: this.packageVersion,
1097
- nodeVersion: process3.version,
1406
+ nodeVersion: process4.version,
1098
1407
  osPlatform: os.platform(),
1099
1408
  osRelease: os.release(),
1100
1409
  osArch: os.arch(),
@@ -1128,7 +1437,7 @@ var TelemetryService = class {
1128
1437
  /** Returns the telemetry client. */
1129
1438
  getClient() {
1130
1439
  if (!this.client) {
1131
- switch (process3.env.SPG_TELEMETRY_PROVIDER) {
1440
+ switch (process4.env.SPG_TELEMETRY_PROVIDER) {
1132
1441
  case "none": {
1133
1442
  this.client = void 0;
1134
1443
  break;
@@ -1220,7 +1529,7 @@ async function waitForUpdateCheck(checkPromise) {
1220
1529
  // package.json
1221
1530
  var package_default = {
1222
1531
  name: "simple-photo-gallery",
1223
- version: "2.0.18"};
1532
+ version: "2.1.0"};
1224
1533
 
1225
1534
  // src/index.ts
1226
1535
  var program = new Command();
@@ -1262,7 +1571,7 @@ function withCommandContext(handler) {
1262
1571
  } catch (error) {
1263
1572
  ui.debug(error);
1264
1573
  errorInfo = error instanceof Error ? { name: error.name, message: error.message } : { name: "UnknownError", message: String(error) };
1265
- process3.exitCode = 1;
1574
+ process4.exitCode = 1;
1266
1575
  }
1267
1576
  const updateInfo = await waitForUpdateCheck(updateCheckPromise);
1268
1577
  if (updateInfo) {
@@ -1284,14 +1593,21 @@ function withCommandContext(handler) {
1284
1593
  program.command("init").description("Initialize a gallery by scaning a folder for images and videos").option(
1285
1594
  "-p, --photos <path>",
1286
1595
  "Path to the folder where the photos are stored. Default: current working directory",
1287
- process3.cwd()
1596
+ process4.cwd()
1288
1597
  ).option(
1289
1598
  "-g, --gallery <path>",
1290
1599
  "Path to the directory where the gallery will be initialized. Default: same directory as the photos folder"
1291
- ).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)));
1292
- 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)));
1293
- 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").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)));
1294
- 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)));
1600
+ ).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)));
1601
+ 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.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)));
1602
+ 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.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(
1603
+ "--theme <package|path>",
1604
+ "Theme package name (e.g., @simple-photo-gallery/theme-modern) or local path (e.g., ./themes/my-theme)"
1605
+ ).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)));
1606
+ 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.cwd()).option("-r, --recursive", "Clean subdirectories recursively", false).action(withCommandContext((options, ui) => clean(options, ui)));
1607
+ 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) => {
1608
+ const handler = withCommandContext((opts, ui) => createTheme({ name, path: opts.path }, ui));
1609
+ await handler(options, command);
1610
+ });
1295
1611
  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)));
1296
1612
  program.parse();
1297
1613
  //# sourceMappingURL=index.js.map