Files
robot-plugins/index.html

726 lines
46 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HRIStudio Robot Plugins</title>
<link rel="stylesheet" href="assets/style.css">
<link rel="icon" type="image/png" id="favicon">
<script>
// Update favicon when logo is loaded
async function updateFavicon() {
try {
const response = await fetch('repository.json');
const data = await response.json();
if (data.assets?.logo) {
document.getElementById('favicon').href = data.assets.logo;
}
} catch (error) {
console.error('Failed to load favicon:', error);
}
}
updateFavicon();
</script>
</head>
<body>
<div class="container">
<div id="loading">Loading repository data...</div>
<div id="content" class="hidden">
<!-- Banner -->
<div id="repoBanner" class="header-banner hidden">
<img alt="Repository Banner">
</div>
<!-- Header Card -->
<div class="card">
<div class="card-content">
<div class="header-content">
<img id="repoIcon" alt="Repository Icon" class="header-icon hidden">
<div class="flex-1">
<div class="flex items-center justify-between">
<h1 id="repoName" class="title"></h1>
<div id="repoLinks" class="button-group"></div>
</div>
<p id="repoDescription" class="description"></p>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<div class="tabs-list" role="tablist">
<button class="tab" role="tab" aria-selected="true" data-state="active" data-tab="overview">Overview</button>
<button class="tab" role="tab" aria-selected="false" data-tab="plugins">Available Plugins</button>
</div>
<!-- Overview Tab -->
<div class="tab-content" data-state="active" role="tabpanel" data-tab="overview" aria-hidden="false">
<div class="grid grid-cols-2">
<!-- Author Info -->
<div class="card">
<div class="card-header">
<h2>Author</h2>
</div>
<div class="card-content">
<div class="card-row">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span class="label">Name:</span>
<span id="authorName"></span>
</div>
<div id="authorOrgContainer" class="card-row hidden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
</svg>
<span class="label">Organization:</span>
<span id="authorOrg"></span>
</div>
<div id="authorEmailContainer" class="card-row hidden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
<span class="label">Email:</span>
<a id="authorEmail" target="_blank" class="text-primary hover:underline"></a>
</div>
<div id="authorUrlContainer" class="card-row hidden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
<span class="label">Website:</span>
<a id="authorUrl" target="_blank" class="text-primary hover:underline">View</a>
</div>
<div id="maintainersContainer" class="card-row hidden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span class="label">Maintainers:</span>
<div id="maintainersList"></div>
</div>
<div class="card-row">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
</svg>
<span class="label">License:</span>
<span id="license"></span>
</div>
<div class="card-row">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span class="label">Last Updated:</span>
<span id="lastUpdated"></span>
</div>
</div>
</div>
<!-- Compatibility -->
<div class="card">
<div class="card-header">
<h2>Compatibility</h2>
</div>
<div class="card-content">
<div class="card-section">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>
HRIStudio
</h3>
<div class="card-row">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-minus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/></svg>
<span class="label">Minimum Version:</span>
<span id="hriMin"></span>
</div>
<div id="hriRecommendedContainer" class="card-row hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></svg>
<span class="label">Recommended:</span>
<span id="hriRecommended"></span>
</div>
</div>
<div id="ros2Container" class="card-section hidden">
<h3>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-grip"><circle cx="12" cy="5" r="1"/><circle cx="19" cy="5" r="1"/><circle cx="5" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/><circle cx="12" cy="19" r="1"/><circle cx="19" cy="19" r="1"/><circle cx="5" cy="19" r="1"/></svg>
ROS 2
</h3>
<div id="ros2Distributions" class="flex flex-wrap gap-1 mb-2"></div>
<div id="ros2RecommendedContainer" class="card-row hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></svg>
<span class="label">Recommended:</span>
<span id="ros2Recommended"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Tags -->
<div class="card" style="margin-top: 1.5rem;">
<div class="card-header">
<h2>Tags</h2>
</div>
<div class="card-content">
<div id="tags" style="margin: -0.25rem;"></div>
</div>
</div>
</div>
<!-- Plugins Tab -->
<div class="tab-content" role="tabpanel" data-tab="plugins" aria-hidden="true">
<div class="card">
<div class="card-header">
<div class="flex items-center justify-between">
<div>
<h2>Available Plugins</h2>
<p class="text-sm text-muted-foreground mt-1">
Browse robot plugins from this repository
</p>
</div>
<span class="badge badge-secondary" id="pluginCount"></span>
</div>
</div>
<div class="card-content">
<div class="plugin-layout">
<!-- Plugin List -->
<div class="plugin-list">
<div id="pluginList"></div>
</div>
<!-- Plugin Details -->
<div class="plugin-details">
<div id="pluginDetails" class="hidden">
<!-- Header -->
<div class="plugin-details-header">
<div class="plugin-details-header-content">
<div class="plugin-details-icon">
<img id="detailsIcon" alt="" class="plugin-icon">
</div>
<div class="flex-1 min-w-0">
<h3 id="detailsTitle" class="text-xl font-semibold">Plugin Title</h3>
<p id="detailsDescription" class="mt-1 text-muted-foreground">Plugin description goes here.</p>
<div class="flex flex-wrap items-center gap-4 mt-4 text-sm">
<div class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
<path d="m5 8 6 6"></path>
<path d="m4 14 2-2-2-2"></path>
<path d="M2 14h4"></path>
<path d="M19 8v8"></path>
<path d="M22 8h-6"></path>
</svg>
<span id="detailsSpeed"></span>
</div>
<div class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
<path d="M3 7v10c0 2 1 3 3 3h12"></path>
<path d="M6 10h14"></path>
<path d="M6 14h14"></path>
<path d="M3 3h18"></path>
</svg>
<span id="detailsBattery"></span>
</div>
<div class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
<path d="M12 20v-8"></path>
<path d="M18 20V4"></path>
<path d="M6 20v-4"></path>
</svg>
<span id="detailsWeight"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="plugin-details-tabs">
<div class="plugin-details-tabs-list" role="tablist">
<button class="plugin-details-tab" role="tab" aria-selected="true" data-state="active" data-plugin-tab="overview">Overview</button>
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="specifications">Specifications</button>
<button class="plugin-details-tab" role="tab" aria-selected="false" data-plugin-tab="actions">Actions</button>
</div>
<!-- Overview Tab -->
<div class="plugin-details-tab-content" data-state="active" role="tabpanel" data-plugin-tab="overview">
<div class="content-section">
<h3>Robot Images</h3>
<div class="image-gallery" id="detailsImages"></div>
</div>
<div class="content-section">
<h3>Documentation</h3>
<div class="grid gap-2 text-sm">
<a
id="detailsMainDocs"
href="#"
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline"
>
User Manual
</a>
<a
id="detailsApiDocs"
href="#"
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline hidden"
>
API Reference
</a>
</div>
</div>
</div>
<!-- Specifications Tab -->
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="specifications">
<div class="content-section">
<h3>Robot Images</h3>
<div class="image-gallery" id="specsImageGallery"></div>
</div>
</div>
<!-- Actions Tab -->
<div class="plugin-details-tab-content" role="tabpanel" data-plugin-tab="actions">
<div class="content-section">
<h3>Actions Tab</h3>
<p>Actions content will go here.</p>
</div>
</div>
</div>
</div>
<div id="emptyState" class="absolute inset-0 flex flex-col items-center justify-center p-6 text-center">
<div class="flex h-16 w-16 items-center justify-center rounded-lg bg-muted mb-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground">
<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/>
<path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>
</svg>
</div>
<h3 class="text-lg font-medium">No Plugin Selected</h3>
<p class="text-sm text-muted-foreground mt-1">
Select a plugin from the list to view its details
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Image Zoom Modal -->
<div class="zoom-modal" id="imageZoomModal" aria-modal="true" role="dialog">
<div class="zoom-modal-content">
<img id="zoomImage" class="zoom-modal-image" alt="Zoomed image">
<button class="zoom-modal-close" id="closeZoomModal" aria-label="Close modal">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
</div>
<script>
// Separate tab management for root and plugin details
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
// Update root tab states
document.querySelectorAll('.tab').forEach(t => {
t.setAttribute('data-state', '');
t.setAttribute('aria-selected', 'false');
});
tab.setAttribute('data-state', 'active');
tab.setAttribute('aria-selected', 'true');
// Update root content states
const tabId = tab.getAttribute('data-tab');
document.querySelectorAll('.tab-content').forEach(content => {
const isActive = content.getAttribute('data-tab') === tabId;
content.setAttribute('data-state', isActive ? 'active' : '');
content.setAttribute('aria-hidden', isActive ? 'false' : 'true');
});
});
});
// Add separate tab management for plugin details
document.querySelectorAll('.plugin-details-tab').forEach(tab => {
tab.addEventListener('click', () => {
// Update plugin details tab states
document.querySelectorAll('.plugin-details-tab').forEach(t => {
t.setAttribute('data-state', '');
t.setAttribute('aria-selected', 'false');
});
tab.setAttribute('data-state', 'active');
tab.setAttribute('aria-selected', 'true');
// Update plugin details content states
const tabId = tab.getAttribute('data-plugin-tab');
document.querySelectorAll('.plugin-details-tab-content').forEach(content => {
const isActive = content.getAttribute('data-plugin-tab') === tabId;
content.setAttribute('data-state', isActive ? 'active' : '');
content.setAttribute('aria-hidden', isActive ? 'false' : 'true');
});
});
});
// Repository data loading
async function loadRepositoryData() {
try {
const response = await fetch('repository.json');
const data = await response.json();
// Update page title
document.title = data.name;
// Update header
document.getElementById('repoName').textContent = data.name;
document.getElementById('repoDescription').textContent = data.description || '';
// Add repository links
const repoLinks = document.getElementById('repoLinks');
if (data.urls?.git) {
const gitButton = document.createElement('a');
gitButton.href = data.urls.git;
gitButton.target = '_blank';
gitButton.rel = 'noopener noreferrer';
gitButton.className = 'button button-outline';
gitButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-git-2"><path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5"/><circle cx="13" cy="12" r="2"/><path d="M18 19c-2.8 0-5-2.2-5-5v8"/><circle cx="20" cy="19" r="2"/></svg>
View Git Repository
`;
repoLinks.appendChild(gitButton);
}
// Load plugins
const pluginsResponse = await fetch('plugins/index.json');
const pluginFiles = await pluginsResponse.json();
// Update plugin count
document.getElementById('pluginCount').textContent = `${pluginFiles.length} ${pluginFiles.length === 1 ? 'plugin' : 'plugins'}`;
// Clear existing plugins
const pluginList = document.getElementById('pluginList');
pluginList.innerHTML = '';
// Load each plugin
for (const pluginFile of pluginFiles) {
const pluginResponse = await fetch(`plugins/${pluginFile}`);
const plugin = await pluginResponse.json();
const card = document.createElement('div');
card.className = 'plugin-list-item';
card.innerHTML = `
<div class="plugin-list-icon">
<img src="${plugin.assets.thumbnailUrl}" alt="${plugin.name}" class="plugin-icon">
</div>
<div class="plugin-list-info">
<div class="flex items-center justify-between">
<div class="plugin-list-title">${plugin.name}</div>
<span class="badge badge-secondary">${plugin.platform}</span>
</div>
<div class="plugin-list-description">${plugin.description || ''}</div>
</div>
`;
// Add click handler
card.addEventListener('click', () => {
// Remove selected class from all items
document.querySelectorAll('.plugin-list-item').forEach(item => {
item.classList.remove('selected');
});
// Add selected class to clicked item
card.classList.add('selected');
// Show plugin details
document.getElementById('pluginDetails').classList.remove('hidden');
document.getElementById('emptyState').classList.add('hidden');
// Update details
document.getElementById('detailsIcon').src = plugin.assets.thumbnailUrl;
document.getElementById('detailsTitle').textContent = plugin.name;
document.getElementById('detailsDescription').textContent = plugin.description || '';
document.getElementById('detailsDocsButton').href = plugin.documentation.mainUrl;
document.getElementById('detailsSpeed').textContent = `${plugin.specs.maxSpeed}m/s`;
document.getElementById('detailsBattery').textContent = `${plugin.specs.batteryLife}h`;
document.getElementById('detailsWeight').textContent = `${plugin.specs.dimensions.weight}kg`;
// Load images in the overview tab
const imagesContainer = document.getElementById('detailsImages');
imagesContainer.innerHTML = `
<div class="image-grid">
<div class="image-item main">
<img src="${plugin.assets.images.main}" alt="${plugin.name} main view">
</div>
${plugin.assets.images.angles ? Object.entries(plugin.assets.images.angles)
.filter(([_, url]) => url)
.map(([angle, url]) => `
<div class="image-item angle">
<img src="${url}" alt="${plugin.name} ${angle} view">
<div class="image-label">${angle} View</div>
</div>
`).join('') : ''}
</div>
`;
// Update specifications
document.getElementById('detailsDimensions').textContent =
`${plugin.specs.dimensions.length}m × ${plugin.specs.dimensions.width}m × ${plugin.specs.dimensions.height}m`;
document.getElementById('detailsSpeedFull').textContent = `${plugin.specs.maxSpeed}m/s`;
document.getElementById('detailsBatteryFull').textContent = `${plugin.specs.batteryLife}h`;
// Update capabilities
const capsContainer = document.getElementById('detailsCapabilities');
capsContainer.innerHTML = '';
plugin.specs.capabilities.forEach(cap => {
const badge = document.createElement('span');
badge.className = 'badge badge-secondary';
badge.textContent = cap;
capsContainer.appendChild(badge);
});
// Update ROS2 config
document.getElementById('detailsNamespace').textContent = plugin.ros2Config.namespace;
document.getElementById('detailsNodePrefix').textContent = plugin.ros2Config.nodePrefix;
const topicsContainer = document.getElementById('detailsTopics');
topicsContainer.innerHTML = '';
Object.entries(plugin.ros2Config.defaultTopics).forEach(([name, topic]) => {
const div = document.createElement('div');
div.innerHTML = `
<span class="text-muted-foreground">${name}: </span>
<code class="rounded bg-muted px-1.5 py-0.5">${topic}</code>
`;
topicsContainer.appendChild(div);
});
// Update documentation links
document.getElementById('detailsMainDocs').href = plugin.documentation.mainUrl;
const apiDocs = document.getElementById('detailsApiDocs');
if (plugin.documentation.apiReference) {
apiDocs.href = plugin.documentation.apiReference;
apiDocs.classList.remove('hidden');
} else {
apiDocs.classList.add('hidden');
}
// Update actions
const actionsContainer = document.getElementById('detailsActions');
actionsContainer.innerHTML = '';
plugin.actions.forEach(action => {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `
<div class="card-content">
<div class="mb-3 flex items-center justify-between">
<h4 class="font-medium">${action.title}</h4>
<span class="badge badge-secondary">${action.type}</span>
</div>
<p class="mb-4 text-sm text-muted-foreground">
${action.description}
</p>
<div class="grid gap-2">
<h5 class="text-sm font-medium text-muted-foreground">Parameters:</h5>
<div class="grid gap-2 pl-4">
${Object.entries(action.parameters.properties).map(([name, prop]) => `
<div class="text-sm">
<span class="font-medium">${prop.title}</span>
${prop.unit ? `<span class="text-muted-foreground"> (${prop.unit})</span>` : ''}
${prop.description ? `<p class="mt-0.5 text-muted-foreground">${prop.description}</p>` : ''}
</div>
`).join('')}
</div>
</div>
</div>
`;
actionsContainer.appendChild(div);
});
// Update specifications tab with image gallery
updateSpecsImageGallery(plugin);
// Trigger click on the Overview tab to show it by default
document.querySelector('.plugin-details-tabs-list .plugin-details-tab[data-plugin-tab="overview"]').click();
});
pluginList.appendChild(card);
}
// Show empty state initially
document.getElementById('pluginDetails').classList.add('hidden');
document.getElementById('emptyState').classList.remove('hidden');
// Update author info
document.getElementById('authorName').textContent = data.author.name;
if (data.author.organization) {
document.getElementById('authorOrgContainer').classList.remove('hidden');
document.getElementById('authorOrg').textContent = data.author.organization;
}
if (data.author.email) {
document.getElementById('authorEmailContainer').classList.remove('hidden');
const emailLink = document.getElementById('authorEmail');
emailLink.href = `mailto:${data.author.email}`;
emailLink.textContent = data.author.email;
}
if (data.author.url) {
document.getElementById('authorUrlContainer').classList.remove('hidden');
document.getElementById('authorUrl').href = data.author.url;
}
// Update maintainers
if (data.maintainers && data.maintainers.length > 0) {
document.getElementById('maintainersContainer').classList.remove('hidden');
const maintainersList = document.getElementById('maintainersList');
maintainersList.innerHTML = data.maintainers.map(maintainer =>
`<span>${maintainer.name}${maintainer.url ?
` (<a href="${maintainer.url}" target="_blank" rel="noopener noreferrer">Git</a>)` :
''}</span>`
).join('');
}
// Update compatibility
document.getElementById('hriMin').textContent = data.compatibility.hristudio.min;
if (data.compatibility.hristudio.recommended) {
document.getElementById('hriRecommendedContainer').classList.remove('hidden');
document.getElementById('hriRecommended').textContent = data.compatibility.hristudio.recommended;
}
if (data.compatibility.ros2) {
document.getElementById('ros2Container').classList.remove('hidden');
const distributionsDiv = document.getElementById('ros2Distributions');
data.compatibility.ros2.distributions.forEach(dist => {
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = dist;
distributionsDiv.appendChild(badge);
});
if (data.compatibility.ros2.recommended) {
document.getElementById('ros2RecommendedContainer').classList.remove('hidden');
document.getElementById('ros2Recommended').textContent = data.compatibility.ros2.recommended;
}
}
// Update tags
const tagsContainer = document.getElementById('tags');
data.tags.forEach(tag => {
const badge = document.createElement('span');
badge.className = 'badge badge-secondary';
badge.textContent = tag;
tagsContainer.appendChild(badge);
});
// Handle assets
if (data.assets?.icon) {
const iconImg = document.getElementById('repoIcon');
iconImg.src = data.assets.icon;
iconImg.classList.remove('hidden');
}
if (data.assets?.banner) {
const banner = document.getElementById('repoBanner');
banner.classList.remove('hidden');
banner.querySelector('img').src = data.assets.banner;
}
// Update license and last updated
document.getElementById('license').textContent = data.license;
const lastUpdated = new Date(data.lastUpdated);
document.getElementById('lastUpdated').textContent = lastUpdated.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Show content
document.getElementById('loading').classList.add('hidden');
document.getElementById('content').classList.remove('hidden');
} catch (error) {
console.error('Error loading repository data:', error);
document.getElementById('loading').textContent = 'Error loading repository data';
}
}
// Load data when page loads
loadRepositoryData();
// Image zoom functionality
const modal = document.getElementById('imageZoomModal');
const zoomImage = document.getElementById('zoomImage');
const closeButton = document.getElementById('closeZoomModal');
function openImageModal(imageUrl, altText) {
zoomImage.src = imageUrl;
zoomImage.alt = altText;
modal.setAttribute('data-state', 'open');
document.body.style.overflow = 'hidden';
}
function closeImageModal() {
modal.removeAttribute('data-state');
document.body.style.overflow = '';
}
// Close modal when clicking outside the image
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeImageModal();
}
});
// Close modal with escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.hasAttribute('data-state')) {
closeImageModal();
}
});
closeButton.addEventListener('click', closeImageModal);
// Update the click handler to include the image gallery
function updateSpecsImageGallery(plugin) {
const gallery = document.getElementById('specsImageGallery');
gallery.innerHTML = '';
// Add main image
const mainImageItem = document.createElement('div');
mainImageItem.className = 'image-gallery-item';
mainImageItem.innerHTML = `
<img src="${plugin.assets.images.main}" alt="${plugin.name} main view">
<div class="image-gallery-label">Main View</div>
`;
mainImageItem.addEventListener('click', () => {
openImageModal(plugin.assets.images.main, `${plugin.name} main view`);
});
gallery.appendChild(mainImageItem);
// Add angle images
if (plugin.assets.images.angles) {
Object.entries(plugin.assets.images.angles)
.filter(([_, url]) => url)
.forEach(([angle, url]) => {
const angleImageItem = document.createElement('div');
angleImageItem.className = 'image-gallery-item';
angleImageItem.innerHTML = `
<img src="${url}" alt="${plugin.name} ${angle} view">
<div class="image-gallery-label">${angle} View</div>
`;
angleImageItem.addEventListener('click', () => {
openImageModal(url, `${plugin.name} ${angle} view`);
});
gallery.appendChild(angleImageItem);
});
}
}
</script>
</body>
</html>