Add metadata button (WIP)
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
@import url('./floating-elements.css');
|
@import url('./floating-elements.css');
|
||||||
@import url('./stats.css');
|
@import url('./stats.css');
|
||||||
@import url('./comparison.css');
|
@import url('./comparison.css');
|
||||||
|
@import url('./metadata.css');
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@import url('./responsive.css');
|
@import url('./responsive.css');
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/* ========================================
|
||||||
|
METADATA POPUP STYLES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Metadata button on thumbnails */
|
||||||
|
.metadata-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
opacity: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item:hover .metadata-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid item positioning for metadata button */
|
||||||
|
.grid-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metadata popup */
|
||||||
|
.metadata-popup {
|
||||||
|
background: var(--card-background);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 250px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--header-background);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-field {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-field:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-key {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-value {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color);
|
||||||
|
word-break: break-word;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-value code {
|
||||||
|
background: var(--code-background);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-tag {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: var(--background-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-more {
|
||||||
|
color: var(--breadcrumb-color);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-metadata {
|
||||||
|
color: var(--breadcrumb-color);
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for metadata popup */
|
||||||
|
.metadata-popup-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup-content::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-popup-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.metadata-popup {
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 14px;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* Metadata Popup functionality for Gallery
|
||||||
|
*
|
||||||
|
* Handles showing metadata in small popups overlaid on plot thumbnails
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MetadataPopup {
|
||||||
|
constructor() {
|
||||||
|
this.activePopup = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Close popup when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.closest('.metadata-btn') && !e.target.closest('.metadata-popup')) {
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close popup on Escape key
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopup(button, plotName, metadata) {
|
||||||
|
// Hide any existing popup
|
||||||
|
this.hidePopup();
|
||||||
|
|
||||||
|
// Create popup element
|
||||||
|
const popup = document.createElement('div');
|
||||||
|
popup.className = 'metadata-popup';
|
||||||
|
popup.innerHTML = this.formatMetadata(plotName, metadata);
|
||||||
|
|
||||||
|
// Position popup relative to button
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
popup.style.position = 'fixed';
|
||||||
|
popup.style.left = rect.left + 'px';
|
||||||
|
popup.style.top = (rect.bottom + 5) + 'px';
|
||||||
|
popup.style.zIndex = '1000';
|
||||||
|
|
||||||
|
// Add to DOM
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
this.activePopup = popup;
|
||||||
|
|
||||||
|
// Adjust position if popup goes off screen
|
||||||
|
setTimeout(() => {
|
||||||
|
const popupRect = popup.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Adjust horizontal position
|
||||||
|
if (popupRect.right > window.innerWidth) {
|
||||||
|
popup.style.left = (rect.right - popupRect.width) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust vertical position
|
||||||
|
if (popupRect.bottom > window.innerHeight) {
|
||||||
|
popup.style.top = (rect.top - popupRect.height - 5) + 'px';
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
popup.classList.add('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePopup() {
|
||||||
|
if (this.activePopup) {
|
||||||
|
this.activePopup.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.activePopup && this.activePopup.parentNode) {
|
||||||
|
this.activePopup.parentNode.removeChild(this.activePopup);
|
||||||
|
}
|
||||||
|
this.activePopup = null;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMetadata(plotName, metadata) {
|
||||||
|
if (!metadata || Object.keys(metadata).length === 0) {
|
||||||
|
return `
|
||||||
|
<div class="metadata-popup-header">
|
||||||
|
<h4>${plotName}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-popup-content">
|
||||||
|
<p class="no-metadata">No metadata available</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="metadata-popup-header">
|
||||||
|
<h4>${plotName}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-popup-content">
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show priority fields first
|
||||||
|
const priorityFields = ['title', 'description', 'plot_type', 'experiment'];
|
||||||
|
const processedKeys = new Set();
|
||||||
|
|
||||||
|
// Display priority fields first
|
||||||
|
for (const key of priorityFields) {
|
||||||
|
if (metadata[key] !== undefined) {
|
||||||
|
html += this.formatMetadataField(key, metadata[key]);
|
||||||
|
processedKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show file info if available
|
||||||
|
if (metadata.file_info) {
|
||||||
|
html += `<div class="metadata-section-title">File Information</div>`;
|
||||||
|
html += this.formatMetadataField('File Size', metadata.file_info.size);
|
||||||
|
if (metadata.file_info.extension) {
|
||||||
|
html += this.formatMetadataField('Format', metadata.file_info.extension);
|
||||||
|
}
|
||||||
|
processedKeys.add('file_info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show timestamps if available
|
||||||
|
if (metadata.timestamps) {
|
||||||
|
html += `<div class="metadata-section-title">Timestamps</div>`;
|
||||||
|
if (metadata.timestamps.created_human) {
|
||||||
|
html += this.formatMetadataField('Created', metadata.timestamps.created_human);
|
||||||
|
}
|
||||||
|
if (metadata.timestamps.modified_human) {
|
||||||
|
html += this.formatMetadataField('Modified', metadata.timestamps.modified_human);
|
||||||
|
}
|
||||||
|
processedKeys.add('timestamps');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show extracted info if available
|
||||||
|
if (metadata.extracted_info && Object.keys(metadata.extracted_info).length > 0) {
|
||||||
|
html += `<div class="metadata-section-title">Plot Details</div>`;
|
||||||
|
for (const [key, value] of Object.entries(metadata.extracted_info)) {
|
||||||
|
html += this.formatMetadataField(key, value);
|
||||||
|
}
|
||||||
|
processedKeys.add('extracted_info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display other fields (excluding generation info unless it's the only data)
|
||||||
|
const otherKeys = Object.keys(metadata).filter(key =>
|
||||||
|
!processedKeys.has(key) && key !== 'generation'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (otherKeys.length > 0) {
|
||||||
|
html += `<div class="metadata-section-title">Additional Information</div>`;
|
||||||
|
for (const key of otherKeys) {
|
||||||
|
html += this.formatMetadataField(key, metadata[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show generation info last if there's no other meaningful data
|
||||||
|
if (processedKeys.size <= 2 && metadata.generation) {
|
||||||
|
html += `<div class="metadata-section-title">Generation Info</div>`;
|
||||||
|
if (metadata.generation.generation_time) {
|
||||||
|
const genDate = new Date(metadata.generation.generation_time);
|
||||||
|
html += this.formatMetadataField('Generated', genDate.toLocaleString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMetadataField(key, value) {
|
||||||
|
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
|
||||||
|
let formattedValue;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
formattedValue = '<em>null</em>';
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length <= 3) {
|
||||||
|
formattedValue = value.map(item => `<span class="metadata-tag">${item}</span>`).join(' ');
|
||||||
|
} else {
|
||||||
|
formattedValue = `${value.slice(0, 3).map(item => `<span class="metadata-tag">${item}</span>`).join(' ')} <span class="metadata-more">+${value.length - 3} more</span>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show object as compact JSON for small objects, or just key count for large ones
|
||||||
|
const keys = Object.keys(value);
|
||||||
|
if (keys.length <= 3) {
|
||||||
|
formattedValue = '<code>' + JSON.stringify(value) + '</code>';
|
||||||
|
} else {
|
||||||
|
formattedValue = `<em>Object with ${keys.length} properties</em>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Truncate long strings
|
||||||
|
const str = String(value);
|
||||||
|
formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="metadata-field">
|
||||||
|
<span class="metadata-key">${displayKey}:</span>
|
||||||
|
<span class="metadata-value">${formattedValue}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global instance
|
||||||
|
window.metadataPopup = new MetadataPopup();
|
||||||
|
|
||||||
|
// Global function for template usage
|
||||||
|
window.showMetadataPopup = function(button, plotName, metadata) {
|
||||||
|
window.metadataPopup.showPopup(button, plotName, metadata);
|
||||||
|
};
|
||||||
@@ -41,6 +41,9 @@
|
|||||||
<a href="{{ item.pdf_href }}">
|
<a href="{{ item.pdf_href }}">
|
||||||
<img src="{{ item.png_href }}" alt="{{ item.name }}">
|
<img src="{{ item.png_href }}" alt="{{ item.name }}">
|
||||||
</a>
|
</a>
|
||||||
|
<button class="metadata-btn" onclick="event.stopPropagation(); showMetadataPopup(this, '{{ item.name|e }}', {{ item.metadata|default('{}')|tojson }})" title="View metadata">
|
||||||
|
⋯
|
||||||
|
</button>
|
||||||
<div class="plot-name" title="{{ item.name }}">{{ item.name }}</div>
|
<div class="plot-name" title="{{ item.name }}">{{ item.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -183,6 +186,9 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Metadata Popup Script -->
|
||||||
|
<script src="{{ assets_path }}/js/metadata-popup.js"></script>
|
||||||
|
|
||||||
<!-- Main JavaScript Application -->
|
<!-- Main JavaScript Application -->
|
||||||
<script type="module" src="{{ assets_path }}/js/main.js"></script>
|
<script type="module" src="{{ assets_path }}/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user