cleanup | integrate site2
This commit is contained in:
parent
2123ae4361
commit
427e57820b
@ -33,6 +33,7 @@ export interface GalleryImage {
|
||||
height?: number
|
||||
width?: number
|
||||
gps?: { lon: number, lat: number }
|
||||
hash?: string
|
||||
}
|
||||
|
||||
export interface MetaJSON {
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import pMap from 'p-map'
|
||||
|
||||
import { GlobOptions } from 'glob'
|
||||
|
||||
import { sanitizeUri } from 'micromark-util-sanitize-uri'
|
||||
import ExifReader from 'exifreader'
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { loadImage } from "imagetools/api"
|
||||
|
||||
@ -15,162 +14,16 @@ import { sync as exists } from '@polymech/fs/exists'
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
|
||||
import { logger } from '@/base/index.js'
|
||||
import { GalleryImage, MetaJSON } from '@/base/images.js'
|
||||
|
||||
import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.js'
|
||||
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL } from '../app/config.js'
|
||||
|
||||
import { ITEM_ASSET_URL, PRODUCT_CONFIG, PRODUCT_ROOT, DEFAULT_IMAGE_URL, FILE_SERVER_DEV } from '../app/config.js'
|
||||
import { GalleryImage, MetaJSON } from './images.js'
|
||||
|
||||
import { env } from './index.js'
|
||||
|
||||
const IMAGES_GLOB = '*.+(JPG|jpg|png|PNG|gif)'
|
||||
const IMAGES_GLOB = '*.+(JPG|jpg|png|jpeg|PNG|gif)'
|
||||
|
||||
// Extract date from EXIF data or file stats
|
||||
export async function extractImageDate(filePath: string): Promise<{ year: number, dateTaken: Date }> {
|
||||
let dateTaken: Date | null = null;
|
||||
|
||||
try {
|
||||
// Try to get EXIF date
|
||||
const exifData = await ExifReader.load(filePath);
|
||||
const dateTimeOriginal = exifData?.['DateTimeOriginal']?.description;
|
||||
const dateTime = exifData?.['DateTime']?.description;
|
||||
const createDate = exifData?.['CreateDate']?.description;
|
||||
|
||||
// Parse EXIF date (format: "YYYY:MM:DD HH:MM:SS")
|
||||
const exifDateString = dateTimeOriginal || dateTime || createDate;
|
||||
if (exifDateString) {
|
||||
const [datePart, timePart] = exifDateString.split(' ');
|
||||
const [year, month, day] = datePart.split(':').map(Number);
|
||||
const [hour, minute, second] = (timePart || '00:00:00').split(':').map(Number);
|
||||
dateTaken = new Date(year, month - 1, day, hour, minute, second);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[extractImageDate] Could not read EXIF data for ${filePath}:`, e);
|
||||
}
|
||||
|
||||
// Fallback to file modification date
|
||||
if (!dateTaken) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
dateTaken = stats.mtime;
|
||||
} catch (e) {
|
||||
console.warn(`[extractImageDate] Could not read file stats for ${filePath}:`, e);
|
||||
dateTaken = new Date(); // Ultimate fallback
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
year: dateTaken.getFullYear(),
|
||||
dateTaken
|
||||
};
|
||||
}
|
||||
|
||||
// Predefined grouping functions
|
||||
export const groupByYear = (image: { year?: number, dateTaken?: Date }) => {
|
||||
const year = image.year || (image.dateTaken ? image.dateTaken.getFullYear() : new Date().getFullYear());
|
||||
return {
|
||||
key: year.toString(),
|
||||
label: year.toString(),
|
||||
sortOrder: year
|
||||
};
|
||||
};
|
||||
|
||||
export const groupByMonth = (image: { year?: number, dateTaken?: Date }) => {
|
||||
const date = image.dateTaken || new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
|
||||
return {
|
||||
key: `${year}-${month.toString().padStart(2, '0')}`,
|
||||
label: `${monthNames[month]} ${year}`,
|
||||
sortOrder: year * 100 + month
|
||||
};
|
||||
};
|
||||
|
||||
// Type for grouping function result
|
||||
export interface GroupInfo {
|
||||
key: string;
|
||||
label: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
// Extract comprehensive metadata from image file
|
||||
export async function extractImageMetadata(filePath: string): Promise<{
|
||||
year: number;
|
||||
dateTaken: Date;
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
exifRaw?: any;
|
||||
}> {
|
||||
const fileName = path.basename(filePath, path.extname(filePath));
|
||||
const baseDir = path.dirname(filePath);
|
||||
|
||||
// Get date information
|
||||
const { year, dateTaken } = await extractImageDate(filePath);
|
||||
|
||||
// Load companion metadata files
|
||||
const metaJsonPath = path.join(baseDir, `${fileName}.json`);
|
||||
const metaMarkdownPath = path.join(baseDir, `${fileName}.md`);
|
||||
|
||||
let metaJson: any = { alt: "", keywords: "", title: "", description: "" };
|
||||
let metaMarkdown = "";
|
||||
|
||||
try {
|
||||
if (fs.existsSync(metaJsonPath)) {
|
||||
metaJson = JSON.parse(fs.readFileSync(metaJsonPath, 'utf8'));
|
||||
}
|
||||
|
||||
if (fs.existsSync(metaMarkdownPath)) {
|
||||
metaMarkdown = fs.readFileSync(metaMarkdownPath, 'utf8');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error loading metadata for ${fileName}:`, e);
|
||||
}
|
||||
|
||||
// Load EXIF data
|
||||
let exifRaw: any = null;
|
||||
try {
|
||||
exifRaw = await ExifReader.load(filePath);
|
||||
} catch (e) {
|
||||
console.warn(`Error loading EXIF data for ${filePath}:`, e);
|
||||
exifRaw = {};
|
||||
}
|
||||
|
||||
// Extract EXIF metadata following the same priority as media.ts gallery function
|
||||
const keywords = exifRaw?.['LastKeywordXMP']?.description || exifRaw?.iptc?.Keywords?.description || '';
|
||||
const exifDescription = exifRaw?.['ImageDescription']?.description || '';
|
||||
const width = exifRaw?.['Image Width']?.value;
|
||||
const height = exifRaw?.['Image Height']?.value;
|
||||
|
||||
// Priority order: EXIF title -> JSON title -> generated from filename
|
||||
const title = exifRaw?.title?.description || metaJson.title ||
|
||||
fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
// Priority order: JSON description -> Markdown -> EXIF ImageDescription -> EXIF IPTC Caption -> empty
|
||||
const description = metaJson.description || metaMarkdown || exifDescription ||
|
||||
exifRaw?.iptc?.['Caption/Abstract']?.description || '';
|
||||
|
||||
return {
|
||||
year,
|
||||
dateTaken,
|
||||
title,
|
||||
description,
|
||||
keywords: keywords ? keywords.split(',').map((k: string) => k.trim()) : [],
|
||||
width,
|
||||
height,
|
||||
exifRaw
|
||||
};
|
||||
}
|
||||
|
||||
export const default_sort = (files: string[]): string[] =>
|
||||
{
|
||||
export const default_sort = (files: string[]): string[] => {
|
||||
const getSortableParts = (filename: string) => {
|
||||
const baseName = path.parse(filename).name;
|
||||
const match = baseName.match(/^(\d+)_?(.*)$/); // Match leading numbers
|
||||
@ -273,7 +126,6 @@ export const gallery = async (
|
||||
}
|
||||
galleryFiles = default_sort(galleryFiles)
|
||||
return await pMap(galleryFiles, async (file: string) => {
|
||||
|
||||
const parts = path.parse(file)
|
||||
const filePath = path.join(mediaPath, file)
|
||||
const meta_path_json = `${mediaPath}/${parts.name}.json`
|
||||
@ -314,6 +166,7 @@ export const gallery = async (
|
||||
const assetUrl = (filePath) => {
|
||||
return sanitizeUri(ITEM_ASSET_URL(
|
||||
{
|
||||
FILE_SERVER_DEV,
|
||||
assetPath,
|
||||
filePath,
|
||||
ITEM_REL: product,
|
||||
@ -321,6 +174,9 @@ export const gallery = async (
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
const fileBuffer = readFileSync(filePath);
|
||||
const hash = createHash('md5').update(fileBuffer).digest('hex').substring(0, 8);
|
||||
const ret: GalleryImage =
|
||||
{
|
||||
name: path.parse(file).name,
|
||||
@ -352,9 +208,9 @@ export const gallery = async (
|
||||
width,
|
||||
height,
|
||||
title,
|
||||
gps: { lon, lat }
|
||||
gps: { lon, lat },
|
||||
hash
|
||||
}
|
||||
|
||||
return ret
|
||||
})
|
||||
}
|
||||
@ -27,7 +27,7 @@ export const removeArrayValues = (obj: any): any => {
|
||||
try {
|
||||
delete obj[key];
|
||||
} catch (e) {
|
||||
debugger
|
||||
//debugger
|
||||
}
|
||||
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
|
||||
@ -1,277 +0,0 @@
|
||||
---
|
||||
|
||||
import { Img } from "imagetools/components";
|
||||
|
||||
import Translate from "@/components/polymech/i18n.astro"
|
||||
|
||||
import { translate } from "@/base/i18n"
|
||||
import { item_keywords } from '@/commons/seo.js'
|
||||
|
||||
import pMap from 'p-map'
|
||||
import { toJsonLd } from '@/base/media.js'
|
||||
import { IComponentConfig } from "@polymech/commons"
|
||||
import { I18N_SOURCE_LANGUAGE, IMAGE_SETTINGS } from "config/config.js"
|
||||
|
||||
interface Image {
|
||||
alt: string
|
||||
src: string
|
||||
title?: string
|
||||
description?: string
|
||||
item?:IComponentConfig
|
||||
}
|
||||
export interface Props {
|
||||
images: Image[];
|
||||
id?: string;
|
||||
siteKeywords?: string[];
|
||||
item?: IComponentConfig;
|
||||
gallerySettings?: {
|
||||
SIZES_REGULAR?: string;
|
||||
SIZES_THUMB?: string;
|
||||
SIZES_LARGE?: string;
|
||||
SHOW_TITLE?: boolean;
|
||||
SHOW_DESCRIPTION?: boolean;
|
||||
};
|
||||
lightboxSettings?: {
|
||||
SIZES_REGULAR?: string;
|
||||
SIZES_THUMB?: string;
|
||||
SIZES_LARGE?: string;
|
||||
SHOW_TITLE?: boolean;
|
||||
SHOW_DESCRIPTION?: boolean;
|
||||
};
|
||||
}
|
||||
const { images, gallerySettings = {}, lightboxSettings = {}, item } = Astro.props;
|
||||
const mergedGallerySettings = {
|
||||
SIZES_REGULAR: gallerySettings.SIZES_REGULAR || IMAGE_SETTINGS.GALLERY.SIZES_REGULAR,
|
||||
SIZES_THUMB: gallerySettings.SIZES_THUMB || IMAGE_SETTINGS.GALLERY.SIZES_THUMB,
|
||||
SHOW_TITLE: gallerySettings.SHOW_TITLE ?? IMAGE_SETTINGS.GALLERY.SHOW_TITLE,
|
||||
SHOW_DESCRIPTION: gallerySettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.GALLERY.SHOW_DESCRIPTION,
|
||||
}
|
||||
const mergedLightboxSettings = {
|
||||
SIZES_LARGE: lightboxSettings.SIZES_LARGE || IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE,
|
||||
SHOW_TITLE: lightboxSettings.SHOW_TITLE ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_TITLE,
|
||||
SHOW_DESCRIPTION: lightboxSettings.SHOW_DESCRIPTION ?? IMAGE_SETTINGS.LIGHTBOX.SHOW_DESCRIPTION,
|
||||
}
|
||||
const locale = Astro.currentLocale || I18N_SOURCE_LANGUAGE
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
|
||||
|
||||
const alt = async (altText) => `${altText} - ${await item_keywords(item, locale)}`
|
||||
|
||||
const alt_translated = await pMap(images, async (image) => {
|
||||
const text = await alt(image.alt)
|
||||
return await translate(text, I18N_SOURCE_LANGUAGE, locale)
|
||||
}, { concurrency: 1 })
|
||||
|
||||
//const json_ld = await pMap(LANGUAGES, async (lang) => {
|
||||
const ld = await toJsonLd(images, Astro.currentLocale || I18N_SOURCE_LANGUAGE, canonicalURL.toString())
|
||||
//}, { concurrency: 1 })
|
||||
|
||||
---
|
||||
|
||||
<div
|
||||
x-data={`
|
||||
{
|
||||
open: false,
|
||||
currentIndex: 0,
|
||||
total: ${images.length},
|
||||
lightboxLoaded: false,
|
||||
touchStartX: 0,
|
||||
touchEndX: 0,
|
||||
mouseStartX: 0,
|
||||
mouseEndX: 0,
|
||||
mouseIsDown: false,
|
||||
minSwipeDistance: 50,
|
||||
isSwiping: false,
|
||||
images: ${JSON.stringify(images)},
|
||||
handleSwipe(isMouseEvent = false) {
|
||||
if (!this.isSwiping && !this.mouseIsDown) return;
|
||||
|
||||
const startX = isMouseEvent ? this.mouseStartX : this.touchStartX;
|
||||
const endX = isMouseEvent ? this.mouseEndX : this.touchEndX;
|
||||
const swipeDistance = endX - startX;
|
||||
|
||||
if (Math.abs(swipeDistance) >= this.minSwipeDistance) {
|
||||
if (swipeDistance > 0 && this.currentIndex > 0) {
|
||||
// Swiped right, show previous image
|
||||
this.currentIndex--;
|
||||
} else if (swipeDistance < 0 && this.currentIndex < this.total - 1) {
|
||||
// Swiped left, show next image
|
||||
this.currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
this.isSwiping = false;
|
||||
this.mouseIsDown = false;
|
||||
},
|
||||
preloadAndOpen(index = null) {
|
||||
// If we're in the middle of a swipe gesture, don't open the lightbox
|
||||
if (this.isSwiping || this.mouseIsDown) return;
|
||||
|
||||
// If an index is provided, update the current index
|
||||
if (index !== null) {
|
||||
this.currentIndex = index;
|
||||
}
|
||||
|
||||
this.lightboxLoaded = false;
|
||||
let img = new Image();
|
||||
img.src = this.images[this.currentIndex].src;
|
||||
this.lightboxLoaded = true;
|
||||
this.open = true;
|
||||
img.onload = () => { };
|
||||
}
|
||||
}
|
||||
`}
|
||||
@keydown.escape.window="open = false"
|
||||
@keydown.window="if(open){
|
||||
if($event.key === 'ArrowRight' && currentIndex < total - 1){
|
||||
currentIndex++;
|
||||
lightboxLoaded = false;
|
||||
preloadAndOpen();
|
||||
} else if($event.key === 'ArrowLeft' && currentIndex > 0){
|
||||
currentIndex--;
|
||||
lightboxLoaded = false;
|
||||
preloadAndOpen();
|
||||
}
|
||||
}"
|
||||
class="product-gallery"><div class="flex flex-col h-full">
|
||||
<!-- Main Image (with swipe functionality) -->
|
||||
<div
|
||||
class="flex-1 p-1 flex items-center justify-center cursor-pointer rounded-lg"
|
||||
@click="preloadAndOpen()"
|
||||
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
|
||||
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
|
||||
@touchcancel="isSwiping = false;"
|
||||
@mousedown="mouseStartX = $event.clientX; mouseIsDown = true; $event.preventDefault();"
|
||||
@mousemove="if(mouseIsDown) { mouseEndX = $event.clientX; }"
|
||||
@mouseup="if(mouseIsDown) { mouseEndX = $event.clientX; handleSwipe(true); }"
|
||||
@mouseleave="mouseIsDown = false;"
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index} class="w-full h-full flex items-center justify-center">
|
||||
<Img
|
||||
src={image.src}
|
||||
alt={alt_translated[index]}
|
||||
objectFit="contain"
|
||||
format="avif"
|
||||
sizes={mergedGallerySettings.SIZES_REGULAR}
|
||||
loading="lazy"
|
||||
attributes={{
|
||||
img: { class: "main-image p-1 rounded-lg max-h-[60vh] aspect-square" }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<!-- Image Info -->
|
||||
{ (mergedGallerySettings.SHOW_TITLE || mergedGallerySettings.SHOW_DESCRIPTION) && (
|
||||
<div class="text-center py-4">
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index}>
|
||||
{ mergedGallerySettings.SHOW_TITLE && ( <h2 id="imageTitle" class="text-xl font-bold">{image.title}</h2>)}
|
||||
{ mergedGallerySettings.SHOW_DESCRIPTION && (<p id="imageDescription" class="text-gray-600"><Translate>{ image.description}</Translate></p>)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<!-- Thumbnails - only shown when multiple images exist -->
|
||||
{images.length > 1 && (
|
||||
<div class="p-1 overflow-x-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-200">
|
||||
<div class="flex p-2 mt-2 ml-2 mr-2 gap-2 items-center justify-center">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={index}
|
||||
x-on:click={`currentIndex = ${index};`}
|
||||
:class={`currentIndex === ${index} ? 'ring-2 ring-orange-500' : ''`}
|
||||
class="thumbnail thumbnail-btn flex-shrink rounded-lg"
|
||||
>
|
||||
<Img
|
||||
src={image.src}
|
||||
objectFit="cover"
|
||||
format="avif"
|
||||
placeholder="blurred"
|
||||
sizes={mergedGallerySettings.SIZES_THUMB}
|
||||
alt={alt_translated[index]}
|
||||
breakpoints={[100]}
|
||||
attributes={{
|
||||
img: {
|
||||
class: "w-32 h-32 rounded-lg hover:ring-2 hover:ring-blue-500 thumbnail-img aspect-square"
|
||||
}
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<!-- Lightbox Modal (with swipe functionality) -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition
|
||||
:class="{ 'lightbox': !lightboxLoaded }"
|
||||
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center lightbox z-10"
|
||||
>
|
||||
<div
|
||||
class="relative max-w-full max-h-full"
|
||||
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
|
||||
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
|
||||
@touchcancel="isSwiping = false;"
|
||||
@mousedown="mouseStartX = $event.clientX; mouseIsDown = true; $event.preventDefault();"
|
||||
@mousemove="if(mouseIsDown) { mouseEndX = $event.clientX; }"
|
||||
@mouseup="if(mouseIsDown) { mouseEndX = $event.clientX; handleSwipe(true); }"
|
||||
@mouseleave="mouseIsDown = false;"
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<div x-show={`currentIndex === ${index}`} key={index}>
|
||||
<Img
|
||||
src={image.src}
|
||||
alt={alt_translated[index]}
|
||||
placeholder="blurred"
|
||||
format="avif"
|
||||
objectFit="contain"
|
||||
sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE}
|
||||
attributes={{
|
||||
img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main" }
|
||||
}}
|
||||
/>
|
||||
{ (mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) &&
|
||||
(image.title || image.description) && (
|
||||
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 m-[8px] max-h-[32vh] p-2 text-white bg-black/50 rounded-lg" style="width: 90%;">
|
||||
{ mergedLightboxSettings.SHOW_TITLE && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
|
||||
{ mergedLightboxSettings.SHOW_DESCRIPTION && (<p><Translate>{image.description}</Translate></p>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
x-on:click="open = false"
|
||||
class="absolute top-0 right-0 text-white text-2xl p-4 m-[8px] bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<!-- Navigation Buttons -->
|
||||
<button
|
||||
x-show="currentIndex > 0"
|
||||
x-on:click="currentIndex--; lightboxLoaded = false; preloadAndOpen();"
|
||||
:disabled="!lightboxLoaded"
|
||||
class="absolute left-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
:class="{'opacity-50 cursor-not-allowed': !lightboxLoaded, 'hover:bg-gray-700/75': lightboxLoaded}"
|
||||
aria-label="Previous"
|
||||
>
|
||||
❮
|
||||
</button>
|
||||
<button
|
||||
x-show="currentIndex < total - 1"
|
||||
x-on:click="currentIndex++; lightboxLoaded = false; preloadAndOpen();"
|
||||
:disabled="!lightboxLoaded"
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2 p-4 m-[8px] text-white text-3xl bg-gray-800/75 bg-opacity-75 rounded-lg lightbox-nav"
|
||||
:class="{'opacity-50 cursor-not-allowed': !lightboxLoaded, 'hover:bg-gray-700/50': lightboxLoaded}"
|
||||
aria-label="Next"
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
<script type="application/ld+json" set:html={ JSON.stringify(ld) }></script>
|
||||
</div>
|
||||
@ -2,11 +2,10 @@
|
||||
import LGallery from "./GalleryK.astro";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { glob } from 'glob';
|
||||
import { glob } from "glob";
|
||||
import { globBase, pathInfo } from "@polymech/commons";
|
||||
import { GalleryImage } from "@/base/images.js";
|
||||
import { gallery } from "@/base/media.js";
|
||||
import { resolveImagePath } from '@/utils/path-resolution';
|
||||
import { resolveImagePath } from "@/utils/path-resolution";
|
||||
|
||||
export interface Props {
|
||||
images?: GalleryImage[];
|
||||
@ -20,16 +19,20 @@ const { images, glob: globPattern, entryPath, ...props } = Astro.props;
|
||||
|
||||
// Get current content directory dynamically from URL
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const pathSegments = currentUrl.split('/').filter(Boolean);
|
||||
const pathSegments = currentUrl.split("/").filter(Boolean);
|
||||
|
||||
// Handle locale-aware URLs (e.g., /en/resources/test/ vs /resources/test/)
|
||||
// Check if first segment is a locale (2-character language code)
|
||||
const isLocaleFirst = pathSegments.length > 0 && pathSegments[0].length === 2 && /^[a-z]{2}$/.test(pathSegments[0]);
|
||||
const isLocaleFirst =
|
||||
pathSegments.length > 0 &&
|
||||
pathSegments[0].length === 2 &&
|
||||
/^[a-z]{2}$/.test(pathSegments[0]);
|
||||
|
||||
// Determine content subdirectory (e.g., 'resources', 'blog', etc.)
|
||||
let contentSubdir = 'resources'; // fallback
|
||||
let contentSubdir = "resources"; // fallback
|
||||
if (pathSegments.length >= 1) {
|
||||
contentSubdir = isLocaleFirst && pathSegments.length > 1
|
||||
contentSubdir =
|
||||
isLocaleFirst && pathSegments.length > 1
|
||||
? pathSegments[1] // Skip locale: /en/resources/test/ -> "resources"
|
||||
: pathSegments[0]; // No locale: /resources/test/ -> "resources"
|
||||
}
|
||||
@ -56,7 +59,7 @@ if (pathSegments.length >= minNestedSegments) {
|
||||
}
|
||||
}
|
||||
|
||||
const contentDir = path.join(process.cwd(), 'src', 'content', contentPath);
|
||||
const contentDir = path.join(process.cwd(), "src", "content", contentPath);
|
||||
|
||||
let allImages: GalleryImage[] = [];
|
||||
|
||||
@ -69,10 +72,17 @@ if (globPattern) {
|
||||
|
||||
// Since gallery expects a specific directory structure, let's use our custom approach
|
||||
// but leverage the media processing logic where possible
|
||||
let matchedFiles = glob.sync(globPattern, { cwd: contentDir, absolute: true });
|
||||
let matchedFiles = glob.sync(globPattern, {
|
||||
cwd: contentDir,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (matchedFiles.length === 0) {
|
||||
const pathInfo2 = pathInfo(globPattern, false, path.join(contentDir, globBase(globPattern).base));
|
||||
const pathInfo2 = pathInfo(
|
||||
globPattern,
|
||||
false,
|
||||
path.join(contentDir, globBase(globPattern).base),
|
||||
);
|
||||
matchedFiles = pathInfo2.FILES;
|
||||
}
|
||||
|
||||
@ -81,7 +91,7 @@ if (globPattern) {
|
||||
matchedFiles.map(async (filePath) => {
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
const fileName = path.basename(filePath, path.extname(filePath));
|
||||
const webPath = `/${relativePath.replace(/\\/g, '/')}`;
|
||||
const webPath = `/${relativePath.replace(/\\/g, "/")}`;
|
||||
|
||||
// Create basic image structure
|
||||
const image: GalleryImage = {
|
||||
@ -91,9 +101,11 @@ if (globPattern) {
|
||||
thumb: webPath,
|
||||
responsive: webPath,
|
||||
alt: `Image: ${fileName}`,
|
||||
title: fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
title: fileName
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase()),
|
||||
description: `Auto-loaded from ${globPattern}`,
|
||||
keywords: '',
|
||||
keywords: "",
|
||||
width: 0,
|
||||
height: 0,
|
||||
gps: { lon: 0, lat: 0 },
|
||||
@ -101,13 +113,13 @@ if (globPattern) {
|
||||
format: path.extname(filePath).toLowerCase().slice(1),
|
||||
width: 0,
|
||||
height: 0,
|
||||
space: '',
|
||||
space: "",
|
||||
channels: 0,
|
||||
depth: '',
|
||||
depth: "",
|
||||
density: 0,
|
||||
chromaSubsampling: '',
|
||||
chromaSubsampling: "",
|
||||
isProgressive: false,
|
||||
resolutionUnit: '',
|
||||
resolutionUnit: "",
|
||||
hasProfile: false,
|
||||
hasAlpha: false,
|
||||
orientation: 0,
|
||||
@ -116,10 +128,10 @@ if (globPattern) {
|
||||
alt: `Image: ${fileName}`,
|
||||
keywords: "",
|
||||
title: "",
|
||||
description: ""
|
||||
description: "",
|
||||
},
|
||||
markdown: "",
|
||||
},
|
||||
markdown: ''
|
||||
}
|
||||
};
|
||||
|
||||
// Try to load companion markdown and JSON files (like media.ts does)
|
||||
@ -129,7 +141,7 @@ if (globPattern) {
|
||||
|
||||
try {
|
||||
if (fs.existsSync(metaJsonPath)) {
|
||||
const metaJson = JSON.parse(fs.readFileSync(metaJsonPath, 'utf8'));
|
||||
const metaJson = JSON.parse(fs.readFileSync(metaJsonPath, "utf8"));
|
||||
image.meta!.json = metaJson;
|
||||
image.alt = metaJson.alt || image.alt;
|
||||
image.title = metaJson.title || image.title;
|
||||
@ -137,7 +149,7 @@ if (globPattern) {
|
||||
}
|
||||
|
||||
if (fs.existsSync(metaMarkdownPath)) {
|
||||
const markdown = fs.readFileSync(metaMarkdownPath, 'utf8');
|
||||
const markdown = fs.readFileSync(metaMarkdownPath, "utf8");
|
||||
image.meta!.markdown = markdown;
|
||||
image.description = markdown || image.description;
|
||||
}
|
||||
@ -146,12 +158,12 @@ if (globPattern) {
|
||||
}
|
||||
|
||||
return image;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
allImages = [...globImages];
|
||||
} catch (error) {
|
||||
console.warn('Glob pattern failed:', error);
|
||||
console.warn("Glob pattern failed:", error);
|
||||
allImages = [];
|
||||
}
|
||||
}
|
||||
@ -162,15 +174,15 @@ if (images) {
|
||||
}
|
||||
|
||||
// Resolve relative paths in all image sources and convert to LGallery format
|
||||
const resolvedImages = allImages.map(image => {
|
||||
const resolvedImages = allImages.map((image) => {
|
||||
const resolvedSrc = resolveImagePath(image.src, entryPath, Astro.url);
|
||||
|
||||
// Convert GalleryImage to LGallery's expected Image interface
|
||||
return {
|
||||
src: resolvedSrc,
|
||||
alt: image.alt || '',
|
||||
alt: image.alt || "",
|
||||
title: image.title,
|
||||
description: image.description
|
||||
description: image.description,
|
||||
};
|
||||
});
|
||||
---
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
---
|
||||
import { DEFAULT_IMAGE_URL } from '@/app/config.js'
|
||||
import { Picture } from "imagetools/components"
|
||||
import { image_url } from "@/base/media.js"
|
||||
import { DEFAULT_IMAGE_URL } from "@/app/config.js";
|
||||
import { Picture } from "imagetools/components";
|
||||
import { image_url } from "../../base/media.js";
|
||||
|
||||
interface SafeImageProps {
|
||||
src: string
|
||||
alt: string
|
||||
fallback?: string
|
||||
[propName: string]: any
|
||||
src: string;
|
||||
alt: string;
|
||||
fallback?: string;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
const {
|
||||
@ -15,8 +15,9 @@ const {
|
||||
alt,
|
||||
fallback = DEFAULT_IMAGE_URL,
|
||||
...rest
|
||||
} = Astro.props as SafeImageProps
|
||||
} = Astro.props as SafeImageProps;
|
||||
|
||||
const srcSafe = await image_url(src, fallback)
|
||||
const srcSafe = await image_url(src, fallback);
|
||||
---
|
||||
|
||||
<Picture class="" src={srcSafe} alt={alt} {...rest} />
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import * as path from 'path'
|
||||
import { findUp } from 'find-up'
|
||||
|
||||
|
||||
|
||||
import { sync as read } from '@polymech/fs/read'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
|
||||
import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
|
||||
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
|
||||
|
||||
import { ContentEntryRenderFunction, ContentEntryType } from 'astro'
|
||||
import { RenderedContent, DataEntry } from "astro:content"
|
||||
import { DataEntry } from "astro:content"
|
||||
import type { Loader, LoaderContext } from 'astro/loaders'
|
||||
|
||||
|
||||
import {
|
||||
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
|
||||
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
|
||||
@ -22,16 +19,16 @@ import {
|
||||
parseBoolean
|
||||
} from 'config/config.js'
|
||||
|
||||
import { env } from '../base/index.js'
|
||||
import { gallery } from '@/base/media.js';
|
||||
import { env } from '@/base/index.js'
|
||||
import { gallery } from '@polymech/astro-base/base/media.js';
|
||||
|
||||
import { get } from '@polymech/commons/component'
|
||||
import { PFilterValid } from '@polymech/commons/filter'
|
||||
|
||||
import { IAssemblyData } from '@polymech/cad'
|
||||
import { logger as log } from '@/base/index.js'
|
||||
|
||||
interface ILoaderContextEx extends LoaderContext {
|
||||
entryTypes: Map<string, ContentEntryType>
|
||||
}
|
||||
interface IComponentConfigEx extends IComponentConfig {
|
||||
content: string
|
||||
@ -42,12 +39,9 @@ interface IComponentConfigEx extends IComponentConfig {
|
||||
}
|
||||
export interface IStoreItem extends DataEntry {
|
||||
data: IComponentConfigEx
|
||||
rendered?: RenderedContent
|
||||
}
|
||||
|
||||
let loaderCtx: ILoaderContextEx
|
||||
|
||||
const renderFunctionByContentType = new WeakMap<ContentEntryType, ContentEntryRenderFunction>();
|
||||
|
||||
const filterBranch = (items: { rel: string, config, path }[],
|
||||
branch: string = RETAIL_PRODUCT_BRANCH) => {
|
||||
@ -88,41 +82,56 @@ const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
*/
|
||||
}
|
||||
|
||||
export const getRenderFunction = async (fileName: string) => {
|
||||
let entryType = loaderCtx.entryTypes.get(path.parse(fileName).ext) as ContentEntryType
|
||||
let _render = renderFunctionByContentType.get(entryType) as ContentEntryRenderFunction
|
||||
const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSchema[]> => {
|
||||
const root = PRODUCT_ROOT()
|
||||
const data: IComponentConfigEx = item.data
|
||||
const itemRel = data.rel
|
||||
const default_profile = env(itemRel)
|
||||
const mainAssemblies = filesEx(root, CAD_MAIN_MATCH(itemRel), { absolute: false })
|
||||
//log.debug(`Loading CAD/CAM for ${itemRel} in ${root} with ${CAD_MAIN_MATCH(itemRel)}`)
|
||||
const cadMeta = mainAssemblies.map((file: string): ICADNodeSchema => {
|
||||
const result: ICADNodeSchema = {
|
||||
file,
|
||||
name: path.basename(file)
|
||||
}
|
||||
CAD_EXTENSIONS.forEach(ext => result[ext] = decodeURIComponent(forward_slash(CAD_URL(file.replace('.SLDASM', ext), data as Record<string, string>))))
|
||||
result.model = path.join(root, file.replace('.SLDASM', CAD_MODEL_EXT))
|
||||
return result
|
||||
})
|
||||
if (!cadMeta.length) {
|
||||
return []
|
||||
}
|
||||
item.data.cad = cadMeta
|
||||
item.data.preview3d = cadMeta[0].html
|
||||
|
||||
if (!_render) {
|
||||
_render = await (entryType as any).getRenderFunction({})
|
||||
renderFunctionByContentType.set(entryType, _render)
|
||||
}
|
||||
return _render
|
||||
}
|
||||
export const renderMarkup = async (content, data: any, fileName: string = 'template.md') => {
|
||||
if (!loaderCtx) {
|
||||
debugger
|
||||
log.error('Loader context not set')
|
||||
if (CAD_EXPORT_CONFIGURATIONS) {
|
||||
const assemblies = cadMeta.filter((assembly) => assembly.model && exists(assembly.model))
|
||||
const components = cadMeta.map((assembly) => {
|
||||
const modelPath = assembly.model as string
|
||||
const model: IAssemblyData = read(modelPath, 'json') as IAssemblyData
|
||||
if (!model) {
|
||||
return
|
||||
}
|
||||
const _render = await getRenderFunction(fileName)
|
||||
if (!_render) {
|
||||
log.error('No render function')
|
||||
const configurations = Object.keys(model.Configurations).filter((c) => {
|
||||
return c !== CAD_DEFAULT_CONFIGURATION &&
|
||||
parseBoolean(model.Configurations[c].Hide || '')
|
||||
})
|
||||
if (!configurations.length ||
|
||||
parseBoolean(model.Configurations?.Global?.Configurations || '')) {
|
||||
return
|
||||
}
|
||||
return _render({
|
||||
body: content,
|
||||
data,
|
||||
filePath: fileName,
|
||||
} as any)
|
||||
log.debug(`Loading CAD/CAM for ${itemRel}`, configurations)
|
||||
})
|
||||
}
|
||||
return cadMeta
|
||||
}
|
||||
|
||||
// Removed obsolete loader types and render functions
|
||||
const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
if (!item || !item.data) {
|
||||
ctx.logger.error(`Error completing ${''}: no data`);
|
||||
return
|
||||
}
|
||||
if(!loaderCtx){
|
||||
loaderCtx = ctx
|
||||
}
|
||||
const { logger } = ctx
|
||||
let data: IComponentConfigEx = item.data
|
||||
|
||||
@ -134,17 +143,18 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
data.product_rel = itemRelMin
|
||||
data.assets = {
|
||||
renderings: [],
|
||||
gallery: []
|
||||
gallery: [],
|
||||
screenshots: []
|
||||
}
|
||||
//////////////////////////////////////////
|
||||
//
|
||||
// Body
|
||||
//
|
||||
let contentPath = path.join(itemDir, 'templates/shared', 'body.md')
|
||||
await getRenderFunction(contentPath)
|
||||
|
||||
if (exists(contentPath)) {
|
||||
data.content = read(contentPath) as string
|
||||
(item as any).filePath = contentPath
|
||||
item.filePath = contentPath
|
||||
}
|
||||
//////////////////////////////////////////
|
||||
//
|
||||
@ -199,6 +209,7 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
//
|
||||
// Extensions, CAD, Media, etc.
|
||||
//
|
||||
data.cad = await cad(item, ctx)
|
||||
|
||||
data.assets.renderings = await gallery('renderings', data.rel) as []
|
||||
data.assets.renderings.length && (data.thumbnail =
|
||||
@ -208,10 +219,12 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||
src: data.assets.renderings[0].thumb
|
||||
})
|
||||
data.assets.gallery = await gallery('media/gallery', data.rel) as []
|
||||
data.assets.main_image = (data.assets.gallery as any[]).find((item: any) => item.src.endsWith('latest.jpg'))
|
||||
data.image = data.assets.renderings[0] || {}
|
||||
|
||||
data.assets.showcase = await gallery('media/showcase', data.rel) as []
|
||||
data.assets.samples = await gallery('media/samples', data.rel) as []
|
||||
data.assets.screenshots = await gallery('media/screenshots', data.rel) as []
|
||||
|
||||
}
|
||||
export function loader(): Loader {
|
||||
@ -221,8 +234,8 @@ export function loader(): Loader {
|
||||
watcher,
|
||||
parseData,
|
||||
store,
|
||||
generateDigest,
|
||||
entryTypes }: ILoaderContextEx) => {
|
||||
generateDigest
|
||||
}: ILoaderContextEx) => {
|
||||
|
||||
store.clear();
|
||||
let products = items({})
|
||||
@ -251,8 +264,7 @@ export function loader(): Loader {
|
||||
watcher,
|
||||
parseData,
|
||||
store,
|
||||
generateDigest,
|
||||
entryTypes
|
||||
generateDigest
|
||||
} as any)
|
||||
storeItem.data['config'] = JSON.stringify({
|
||||
...storeItem.data
|
||||
|
||||
Loading…
Reference in New Issue
Block a user