mirror of
https://github.com/soconnor0919/robot-plugins.git
synced 2025-12-11 06:34:44 -05:00
726 lines
46 KiB
HTML
Executable File
726 lines
46 KiB
HTML
Executable File
<!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> |