cleanup | integrate site2
This commit is contained in:
parent
2123ae4361
commit
427e57820b
@ -33,6 +33,7 @@ export interface GalleryImage {
|
|||||||
height?: number
|
height?: number
|
||||||
width?: number
|
width?: number
|
||||||
gps?: { lon: number, lat: number }
|
gps?: { lon: number, lat: number }
|
||||||
|
hash?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetaJSON {
|
export interface MetaJSON {
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import pMap from 'p-map'
|
import pMap from 'p-map'
|
||||||
|
|
||||||
import { GlobOptions } from 'glob'
|
import { GlobOptions } from 'glob'
|
||||||
|
|
||||||
import { sanitizeUri } from 'micromark-util-sanitize-uri'
|
import { sanitizeUri } from 'micromark-util-sanitize-uri'
|
||||||
import ExifReader from 'exifreader'
|
import ExifReader from 'exifreader'
|
||||||
import fs from 'node:fs'
|
|
||||||
|
|
||||||
import { loadImage } from "imagetools/api"
|
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 { sync as read } from '@polymech/fs/read'
|
||||||
|
|
||||||
import { logger } from '@/base/index.js'
|
import { logger } from '@/base/index.js'
|
||||||
import { GalleryImage, MetaJSON } from '@/base/images.js'
|
|
||||||
|
|
||||||
import { removeArrayValues, removeArrays, removeBufferValues, removeEmptyObjects } from '@/base/objects.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'
|
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 const default_sort = (files: string[]): string[] => {
|
||||||
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[] =>
|
|
||||||
{
|
|
||||||
const getSortableParts = (filename: string) => {
|
const getSortableParts = (filename: string) => {
|
||||||
const baseName = path.parse(filename).name;
|
const baseName = path.parse(filename).name;
|
||||||
const match = baseName.match(/^(\d+)_?(.*)$/); // Match leading numbers
|
const match = baseName.match(/^(\d+)_?(.*)$/); // Match leading numbers
|
||||||
@ -273,7 +126,6 @@ export const gallery = async (
|
|||||||
}
|
}
|
||||||
galleryFiles = default_sort(galleryFiles)
|
galleryFiles = default_sort(galleryFiles)
|
||||||
return await pMap(galleryFiles, async (file: string) => {
|
return await pMap(galleryFiles, async (file: string) => {
|
||||||
|
|
||||||
const parts = path.parse(file)
|
const parts = path.parse(file)
|
||||||
const filePath = path.join(mediaPath, file)
|
const filePath = path.join(mediaPath, file)
|
||||||
const meta_path_json = `${mediaPath}/${parts.name}.json`
|
const meta_path_json = `${mediaPath}/${parts.name}.json`
|
||||||
@ -314,6 +166,7 @@ export const gallery = async (
|
|||||||
const assetUrl = (filePath) => {
|
const assetUrl = (filePath) => {
|
||||||
return sanitizeUri(ITEM_ASSET_URL(
|
return sanitizeUri(ITEM_ASSET_URL(
|
||||||
{
|
{
|
||||||
|
FILE_SERVER_DEV,
|
||||||
assetPath,
|
assetPath,
|
||||||
filePath,
|
filePath,
|
||||||
ITEM_REL: product,
|
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 =
|
const ret: GalleryImage =
|
||||||
{
|
{
|
||||||
name: path.parse(file).name,
|
name: path.parse(file).name,
|
||||||
@ -352,9 +208,9 @@ export const gallery = async (
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
title,
|
title,
|
||||||
gps: { lon, lat }
|
gps: { lon, lat },
|
||||||
|
hash
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ export const removeArrayValues = (obj: any): any => {
|
|||||||
try {
|
try {
|
||||||
delete obj[key];
|
delete obj[key];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugger
|
//debugger
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (typeof obj[key] === 'object') {
|
} 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 LGallery from "./GalleryK.astro";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { glob } from 'glob';
|
import { glob } from "glob";
|
||||||
import { globBase, pathInfo } from "@polymech/commons";
|
import { globBase, pathInfo } from "@polymech/commons";
|
||||||
import { GalleryImage } from "@/base/images.js";
|
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 {
|
export interface Props {
|
||||||
images?: GalleryImage[];
|
images?: GalleryImage[];
|
||||||
@ -20,24 +19,28 @@ const { images, glob: globPattern, entryPath, ...props } = Astro.props;
|
|||||||
|
|
||||||
// Get current content directory dynamically from URL
|
// Get current content directory dynamically from URL
|
||||||
const currentUrl = Astro.url.pathname;
|
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/)
|
// Handle locale-aware URLs (e.g., /en/resources/test/ vs /resources/test/)
|
||||||
// Check if first segment is a locale (2-character language code)
|
// 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.)
|
// Determine content subdirectory (e.g., 'resources', 'blog', etc.)
|
||||||
let contentSubdir = 'resources'; // fallback
|
let contentSubdir = "resources"; // fallback
|
||||||
if (pathSegments.length >= 1) {
|
if (pathSegments.length >= 1) {
|
||||||
contentSubdir = isLocaleFirst && pathSegments.length > 1
|
contentSubdir =
|
||||||
? pathSegments[1] // Skip locale: /en/resources/test/ -> "resources"
|
isLocaleFirst && pathSegments.length > 1
|
||||||
: pathSegments[0]; // No locale: /resources/test/ -> "resources"
|
? pathSegments[1] // Skip locale: /en/resources/test/ -> "resources"
|
||||||
|
: pathSegments[0]; // No locale: /resources/test/ -> "resources"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the nested content directory (e.g., 'cassandra' from /resources/cassandra/home/)
|
// Get the nested content directory (e.g., 'cassandra' from /resources/cassandra/home/)
|
||||||
// We need to distinguish between:
|
// We need to distinguish between:
|
||||||
// 1. /resources/test -> root-level file test.mdx -> contentPath = "resources"
|
// 1. /resources/test -> root-level file test.mdx -> contentPath = "resources"
|
||||||
// 2. /es/resources/test -> root-level file test.mdx with locale -> contentPath = "resources"
|
// 2. /es/resources/test -> root-level file test.mdx with locale -> contentPath = "resources"
|
||||||
// 3. /resources/cassandra/home -> nested file cassandra/home.mdx -> contentPath = "resources/cassandra"
|
// 3. /resources/cassandra/home -> nested file cassandra/home.mdx -> contentPath = "resources/cassandra"
|
||||||
// 4. /es/resources/cassandra/home -> nested file with locale -> contentPath = "resources/cassandra"
|
// 4. /es/resources/cassandra/home -> nested file with locale -> contentPath = "resources/cassandra"
|
||||||
let contentPath = contentSubdir;
|
let contentPath = contentSubdir;
|
||||||
@ -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[] = [];
|
let allImages: GalleryImage[] = [];
|
||||||
|
|
||||||
@ -66,13 +69,20 @@ if (globPattern) {
|
|||||||
// For content collections, we need to create a mock product structure
|
// For content collections, we need to create a mock product structure
|
||||||
// that the gallery function can work with
|
// that the gallery function can work with
|
||||||
const mockProductPath = `content/${contentSubdir}`;
|
const mockProductPath = `content/${contentSubdir}`;
|
||||||
|
|
||||||
// Since gallery expects a specific directory structure, let's use our custom approach
|
// Since gallery expects a specific directory structure, let's use our custom approach
|
||||||
// but leverage the media processing logic where possible
|
// 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) {
|
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;
|
matchedFiles = pathInfo2.FILES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,8 +91,8 @@ if (globPattern) {
|
|||||||
matchedFiles.map(async (filePath) => {
|
matchedFiles.map(async (filePath) => {
|
||||||
const relativePath = path.relative(process.cwd(), filePath);
|
const relativePath = path.relative(process.cwd(), filePath);
|
||||||
const fileName = path.basename(filePath, path.extname(filePath));
|
const fileName = path.basename(filePath, path.extname(filePath));
|
||||||
const webPath = `/${relativePath.replace(/\\/g, '/')}`;
|
const webPath = `/${relativePath.replace(/\\/g, "/")}`;
|
||||||
|
|
||||||
// Create basic image structure
|
// Create basic image structure
|
||||||
const image: GalleryImage = {
|
const image: GalleryImage = {
|
||||||
name: fileName,
|
name: fileName,
|
||||||
@ -91,9 +101,11 @@ if (globPattern) {
|
|||||||
thumb: webPath,
|
thumb: webPath,
|
||||||
responsive: webPath,
|
responsive: webPath,
|
||||||
alt: `Image: ${fileName}`,
|
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}`,
|
description: `Auto-loaded from ${globPattern}`,
|
||||||
keywords: '',
|
keywords: "",
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
gps: { lon: 0, lat: 0 },
|
gps: { lon: 0, lat: 0 },
|
||||||
@ -101,13 +113,13 @@ if (globPattern) {
|
|||||||
format: path.extname(filePath).toLowerCase().slice(1),
|
format: path.extname(filePath).toLowerCase().slice(1),
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
space: '',
|
space: "",
|
||||||
channels: 0,
|
channels: 0,
|
||||||
depth: '',
|
depth: "",
|
||||||
density: 0,
|
density: 0,
|
||||||
chromaSubsampling: '',
|
chromaSubsampling: "",
|
||||||
isProgressive: false,
|
isProgressive: false,
|
||||||
resolutionUnit: '',
|
resolutionUnit: "",
|
||||||
hasProfile: false,
|
hasProfile: false,
|
||||||
hasAlpha: false,
|
hasAlpha: false,
|
||||||
orientation: 0,
|
orientation: 0,
|
||||||
@ -116,42 +128,42 @@ if (globPattern) {
|
|||||||
alt: `Image: ${fileName}`,
|
alt: `Image: ${fileName}`,
|
||||||
keywords: "",
|
keywords: "",
|
||||||
title: "",
|
title: "",
|
||||||
description: ""
|
description: "",
|
||||||
},
|
},
|
||||||
markdown: ''
|
markdown: "",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to load companion markdown and JSON files (like media.ts does)
|
// Try to load companion markdown and JSON files (like media.ts does)
|
||||||
const baseDir = path.dirname(filePath);
|
const baseDir = path.dirname(filePath);
|
||||||
const metaJsonPath = path.join(baseDir, `${fileName}.json`);
|
const metaJsonPath = path.join(baseDir, `${fileName}.json`);
|
||||||
const metaMarkdownPath = path.join(baseDir, `${fileName}.md`);
|
const metaMarkdownPath = path.join(baseDir, `${fileName}.md`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(metaJsonPath)) {
|
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.meta!.json = metaJson;
|
||||||
image.alt = metaJson.alt || image.alt;
|
image.alt = metaJson.alt || image.alt;
|
||||||
image.title = metaJson.title || image.title;
|
image.title = metaJson.title || image.title;
|
||||||
image.description = metaJson.description || image.description;
|
image.description = metaJson.description || image.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(metaMarkdownPath)) {
|
if (fs.existsSync(metaMarkdownPath)) {
|
||||||
const markdown = fs.readFileSync(metaMarkdownPath, 'utf8');
|
const markdown = fs.readFileSync(metaMarkdownPath, "utf8");
|
||||||
image.meta!.markdown = markdown;
|
image.meta!.markdown = markdown;
|
||||||
image.description = markdown || image.description;
|
image.description = markdown || image.description;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Error loading metadata for ${fileName}:`, e);
|
console.warn(`Error loading metadata for ${fileName}:`, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
allImages = [...globImages];
|
allImages = [...globImages];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Glob pattern failed:', error);
|
console.warn("Glob pattern failed:", error);
|
||||||
allImages = [];
|
allImages = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,15 +174,15 @@ if (images) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve relative paths in all image sources and convert to LGallery format
|
// 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);
|
const resolvedSrc = resolveImagePath(image.src, entryPath, Astro.url);
|
||||||
|
|
||||||
// Convert GalleryImage to LGallery's expected Image interface
|
// Convert GalleryImage to LGallery's expected Image interface
|
||||||
return {
|
return {
|
||||||
src: resolvedSrc,
|
src: resolvedSrc,
|
||||||
alt: image.alt || '',
|
alt: image.alt || "",
|
||||||
title: image.title,
|
title: image.title,
|
||||||
description: image.description
|
description: image.description,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
import { DEFAULT_IMAGE_URL } from '@/app/config.js'
|
import { DEFAULT_IMAGE_URL } from "@/app/config.js";
|
||||||
import { Picture } from "imagetools/components"
|
import { Picture } from "imagetools/components";
|
||||||
import { image_url } from "@/base/media.js"
|
import { image_url } from "../../base/media.js";
|
||||||
|
|
||||||
interface SafeImageProps {
|
interface SafeImageProps {
|
||||||
src: string
|
src: string;
|
||||||
alt: string
|
alt: string;
|
||||||
fallback?: string
|
fallback?: string;
|
||||||
[propName: string]: any
|
[propName: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -15,8 +15,9 @@ const {
|
|||||||
alt,
|
alt,
|
||||||
fallback = DEFAULT_IMAGE_URL,
|
fallback = DEFAULT_IMAGE_URL,
|
||||||
...rest
|
...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} />
|
<Picture class="" src={srcSafe} alt={alt} {...rest} />
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import { findUp } from 'find-up'
|
import { findUp } from 'find-up'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { sync as read } from '@polymech/fs/read'
|
import { sync as read } from '@polymech/fs/read'
|
||||||
import { sync as exists } from '@polymech/fs/exists'
|
import { sync as exists } from '@polymech/fs/exists'
|
||||||
|
|
||||||
import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
|
import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
|
||||||
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
|
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
|
||||||
|
|
||||||
import { ContentEntryRenderFunction, ContentEntryType } from 'astro'
|
import { DataEntry } from "astro:content"
|
||||||
import { RenderedContent, DataEntry } from "astro:content"
|
|
||||||
import type { Loader, LoaderContext } from 'astro/loaders'
|
import type { Loader, LoaderContext } from 'astro/loaders'
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
|
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
|
||||||
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
|
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
|
||||||
@ -22,16 +19,16 @@ import {
|
|||||||
parseBoolean
|
parseBoolean
|
||||||
} from 'config/config.js'
|
} from 'config/config.js'
|
||||||
|
|
||||||
import { env } from '../base/index.js'
|
import { env } from '@/base/index.js'
|
||||||
import { gallery } from '@/base/media.js';
|
import { gallery } from '@polymech/astro-base/base/media.js';
|
||||||
|
|
||||||
import { get } from '@polymech/commons/component'
|
import { get } from '@polymech/commons/component'
|
||||||
import { PFilterValid } from '@polymech/commons/filter'
|
import { PFilterValid } from '@polymech/commons/filter'
|
||||||
|
|
||||||
|
import { IAssemblyData } from '@polymech/cad'
|
||||||
import { logger as log } from '@/base/index.js'
|
import { logger as log } from '@/base/index.js'
|
||||||
|
|
||||||
interface ILoaderContextEx extends LoaderContext {
|
interface ILoaderContextEx extends LoaderContext {
|
||||||
entryTypes: Map<string, ContentEntryType>
|
|
||||||
}
|
}
|
||||||
interface IComponentConfigEx extends IComponentConfig {
|
interface IComponentConfigEx extends IComponentConfig {
|
||||||
content: string
|
content: string
|
||||||
@ -42,12 +39,9 @@ interface IComponentConfigEx extends IComponentConfig {
|
|||||||
}
|
}
|
||||||
export interface IStoreItem extends DataEntry {
|
export interface IStoreItem extends DataEntry {
|
||||||
data: IComponentConfigEx
|
data: IComponentConfigEx
|
||||||
rendered?: RenderedContent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let loaderCtx: ILoaderContextEx
|
|
||||||
|
|
||||||
const renderFunctionByContentType = new WeakMap<ContentEntryType, ContentEntryRenderFunction>();
|
|
||||||
|
|
||||||
const filterBranch = (items: { rel: string, config, path }[],
|
const filterBranch = (items: { rel: string, config, path }[],
|
||||||
branch: string = RETAIL_PRODUCT_BRANCH) => {
|
branch: string = RETAIL_PRODUCT_BRANCH) => {
|
||||||
@ -88,41 +82,56 @@ const onComponent = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRenderFunction = async (fileName: string) => {
|
const cad = async (item: IStoreItem, ctx: ILoaderContextEx): Promise<ICADNodeSchema[]> => {
|
||||||
let entryType = loaderCtx.entryTypes.get(path.parse(fileName).ext) as ContentEntryType
|
const root = PRODUCT_ROOT()
|
||||||
let _render = renderFunctionByContentType.get(entryType) as ContentEntryRenderFunction
|
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) {
|
if (CAD_EXPORT_CONFIGURATIONS) {
|
||||||
_render = await (entryType as any).getRenderFunction({})
|
const assemblies = cadMeta.filter((assembly) => assembly.model && exists(assembly.model))
|
||||||
renderFunctionByContentType.set(entryType, _render)
|
const components = cadMeta.map((assembly) => {
|
||||||
|
const modelPath = assembly.model as string
|
||||||
|
const model: IAssemblyData = read(modelPath, 'json') as IAssemblyData
|
||||||
|
if (!model) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
log.debug(`Loading CAD/CAM for ${itemRel}`, configurations)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return _render
|
return cadMeta
|
||||||
}
|
|
||||||
export const renderMarkup = async (content, data: any, fileName: string = 'template.md') => {
|
|
||||||
if (!loaderCtx) {
|
|
||||||
debugger
|
|
||||||
log.error('Loader context not set')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const _render = await getRenderFunction(fileName)
|
|
||||||
if (!_render) {
|
|
||||||
log.error('No render function')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return _render({
|
|
||||||
body: content,
|
|
||||||
data,
|
|
||||||
filePath: fileName,
|
|
||||||
} as any)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Removed obsolete loader types and render functions
|
||||||
const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
||||||
if (!item || !item.data) {
|
if (!item || !item.data) {
|
||||||
ctx.logger.error(`Error completing ${''}: no data`);
|
ctx.logger.error(`Error completing ${''}: no data`);
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if(!loaderCtx){
|
|
||||||
loaderCtx = ctx
|
|
||||||
}
|
|
||||||
const { logger } = ctx
|
const { logger } = ctx
|
||||||
let data: IComponentConfigEx = item.data
|
let data: IComponentConfigEx = item.data
|
||||||
|
|
||||||
@ -134,17 +143,18 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
|||||||
data.product_rel = itemRelMin
|
data.product_rel = itemRelMin
|
||||||
data.assets = {
|
data.assets = {
|
||||||
renderings: [],
|
renderings: [],
|
||||||
gallery: []
|
gallery: [],
|
||||||
|
screenshots: []
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Body
|
// Body
|
||||||
//
|
//
|
||||||
let contentPath = path.join(itemDir, 'templates/shared', 'body.md')
|
let contentPath = path.join(itemDir, 'templates/shared', 'body.md')
|
||||||
await getRenderFunction(contentPath)
|
|
||||||
if (exists(contentPath)) {
|
if (exists(contentPath)) {
|
||||||
data.content = read(contentPath) as string
|
data.content = read(contentPath) as string
|
||||||
(item as any).filePath = contentPath
|
item.filePath = contentPath
|
||||||
}
|
}
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@ -159,7 +169,7 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
|||||||
let resourcesDefaultPath = await findUp('resources.md', {
|
let resourcesDefaultPath = await findUp('resources.md', {
|
||||||
stopAt: PRODUCT_ROOT(),
|
stopAt: PRODUCT_ROOT(),
|
||||||
cwd: itemDir
|
cwd: itemDir
|
||||||
}) || ""
|
}) || ""
|
||||||
exists(resourcesDefaultPath) && (data.shared_resources = read(resourcesDefaultPath) as string || "")
|
exists(resourcesDefaultPath) && (data.shared_resources = read(resourcesDefaultPath) as string || "")
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@ -199,7 +209,8 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
|||||||
//
|
//
|
||||||
// Extensions, CAD, Media, etc.
|
// Extensions, CAD, Media, etc.
|
||||||
//
|
//
|
||||||
|
data.cad = await cad(item, ctx)
|
||||||
|
|
||||||
data.assets.renderings = await gallery('renderings', data.rel) as []
|
data.assets.renderings = await gallery('renderings', data.rel) as []
|
||||||
data.assets.renderings.length && (data.thumbnail =
|
data.assets.renderings.length && (data.thumbnail =
|
||||||
{
|
{
|
||||||
@ -208,10 +219,12 @@ const onItem = async (item: IStoreItem, ctx: ILoaderContextEx) => {
|
|||||||
src: data.assets.renderings[0].thumb
|
src: data.assets.renderings[0].thumb
|
||||||
})
|
})
|
||||||
data.assets.gallery = await gallery('media/gallery', data.rel) as []
|
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.image = data.assets.renderings[0] || {}
|
||||||
|
|
||||||
data.assets.showcase = await gallery('media/showcase', data.rel) as []
|
data.assets.showcase = await gallery('media/showcase', data.rel) as []
|
||||||
data.assets.samples = await gallery('media/samples', 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 {
|
export function loader(): Loader {
|
||||||
@ -221,8 +234,8 @@ export function loader(): Loader {
|
|||||||
watcher,
|
watcher,
|
||||||
parseData,
|
parseData,
|
||||||
store,
|
store,
|
||||||
generateDigest,
|
generateDigest
|
||||||
entryTypes }: ILoaderContextEx) => {
|
}: ILoaderContextEx) => {
|
||||||
|
|
||||||
store.clear();
|
store.clear();
|
||||||
let products = items({})
|
let products = items({})
|
||||||
@ -251,8 +264,7 @@ export function loader(): Loader {
|
|||||||
watcher,
|
watcher,
|
||||||
parseData,
|
parseData,
|
||||||
store,
|
store,
|
||||||
generateDigest,
|
generateDigest
|
||||||
entryTypes
|
|
||||||
} as any)
|
} as any)
|
||||||
storeItem.data['config'] = JSON.stringify({
|
storeItem.data['config'] = JSON.stringify({
|
||||||
...storeItem.data
|
...storeItem.data
|
||||||
@ -264,4 +276,4 @@ export function loader(): Loader {
|
|||||||
name: "store-loader",
|
name: "store-loader",
|
||||||
load
|
load
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user