Okay Wish I could say I know exactly. Will do better next time promise lol
This commit is contained in:
437
mkdocs/docs/assets/css/image-gallery.css
Normal file
437
mkdocs/docs/assets/css/image-gallery.css
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* Image Gallery Styles for MkDocs
|
||||
*
|
||||
* Ensures photo blocks from landing pages render properly in documentation.
|
||||
* Mirrors video-player.css structure with green/blue photo theme.
|
||||
*/
|
||||
|
||||
/* Photo block container */
|
||||
.photo-block {
|
||||
margin: 2rem auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.photo-block img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .photo-block img {
|
||||
background: #1b2838;
|
||||
}
|
||||
|
||||
/* Photo caption */
|
||||
.photo-caption {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .photo-caption {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Photo card block */
|
||||
.photo-card-block {
|
||||
margin: 2rem auto;
|
||||
max-width: 480px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.photo-card-block:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.photo-card-block a {
|
||||
text-decoration: none !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Photo card gallery button */
|
||||
.photo-card-gallery-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
/* Photo album block */
|
||||
.photo-album-block {
|
||||
margin: 2rem auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.photo-album-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Photo album grid */
|
||||
.photo-album-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.photo-album-grid.cols-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.photo-album-grid.cols-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.photo-album-grid.cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
/* Photo album cell */
|
||||
.photo-album-cell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #f0f0f0;
|
||||
aspect-ratio: 1;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.photo-album-cell:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .photo-album-cell {
|
||||
background: #1b2838;
|
||||
}
|
||||
|
||||
.photo-album-cell img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.photo-album-cell a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Album "View full album" link */
|
||||
.photo-album-footer {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.photo-album-footer a {
|
||||
color: #43cea2;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.photo-album-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .photo-album-footer a {
|
||||
color: #43cea2;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.photo-loading {
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .photo-loading {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
.photo-error {
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
background: #fff3f3;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 8px;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .photo-error {
|
||||
background: rgba(207, 19, 34, 0.1);
|
||||
border-color: rgba(207, 19, 34, 0.3);
|
||||
}
|
||||
|
||||
.photo-error svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.photo-error p {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Photo placeholder (shown during GrapesJS export before hydration) */
|
||||
.photo-placeholder {
|
||||
aspect-ratio: 3/2;
|
||||
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.photo-placeholder svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.photo-placeholder p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.photo-block,
|
||||
.photo-card-block,
|
||||
.photo-album-block {
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
.photo-block img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.photo-card-block {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.photo-album-grid.cols-4 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.photo-album-grid.cols-3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.photo-album-grid.cols-4,
|
||||
.photo-album-grid.cols-3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Photo Album Carousel ─────────────────────────────────────────── */
|
||||
|
||||
.photo-album-carousel {
|
||||
margin: 2rem auto;
|
||||
max-width: 900px;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.carousel-viewport {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .carousel-viewport {
|
||||
background: #1b2838;
|
||||
}
|
||||
|
||||
.carousel-track {
|
||||
display: flex;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.carousel-slide a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.carousel-slide img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 560px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* Navigation arrows */
|
||||
.carousel-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.carousel-viewport:hover .carousel-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carousel-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.carousel-btn-prev {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.carousel-btn-next {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
/* Dot indicators */
|
||||
.carousel-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
|
||||
.carousel-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.carousel-dot:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.carousel-dot.active {
|
||||
background: #43cea2;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .carousel-dot {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .carousel-dot:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .carousel-dot.active {
|
||||
background: #43cea2;
|
||||
}
|
||||
|
||||
/* Responsive: arrows always visible on mobile (no hover) */
|
||||
@media (max-width: 768px) {
|
||||
.carousel-btn {
|
||||
opacity: 1;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.carousel-btn-prev {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.carousel-btn-next {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.carousel-slide img {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.photo-album-carousel {
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.carousel-slide img {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.carousel-dots {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.carousel-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure photos respect content width in Material theme */
|
||||
.md-content .photo-block,
|
||||
.md-content .photo-card-block,
|
||||
.md-content .photo-album-block,
|
||||
.md-content .photo-album-carousel {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Accessibility: Focus styles */
|
||||
.photo-album-cell a:focus,
|
||||
.photo-card-block a:focus {
|
||||
outline: 2px solid var(--md-primary-fg-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.carousel-btn:focus-visible {
|
||||
outline: 2px solid var(--md-primary-fg-color);
|
||||
outline-offset: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carousel-dot:focus-visible {
|
||||
outline: 2px solid var(--md-primary-fg-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.carousel-slide a:focus-visible {
|
||||
outline: 2px solid var(--md-primary-fg-color);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
611
mkdocs/docs/assets/js/image-gallery.js
Normal file
611
mkdocs/docs/assets/js/image-gallery.js
Normal file
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* Image Gallery Hydration for MkDocs
|
||||
*
|
||||
* Automatically converts .photo-block, .photo-card-block, and .photo-album-block
|
||||
* placeholders into actual images/galleries when landing pages with photo blocks
|
||||
* are exported to MkDocs.
|
||||
*
|
||||
* Uses the same MEDIA_API_URL and PUBLIC_URL globals as video-player.js
|
||||
* (injected by hooks/env_config_hook.py).
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuration — same globals as video-player.js
|
||||
var MEDIA_API_URL = window.MEDIA_API_URL || 'http://localhost:4100';
|
||||
var PUBLIC_URL = window.PUBLIC_URL || 'http://localhost:3000';
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(unsafe) {
|
||||
return String(unsafe)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format view count (e.g. 1200 -> "1.2K views")
|
||||
*/
|
||||
function formatViewCount(count) {
|
||||
if (!count || count <= 0) return '0 views';
|
||||
if (count === 1) return '1 view';
|
||||
if (count < 1000) return count + ' views';
|
||||
if (count < 1000000) return (count / 1000).toFixed(1).replace(/\.0$/, '') + 'K views';
|
||||
return (count / 1000000).toFixed(1).replace(/\.0$/, '') + 'M views';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gallery URL for a photo
|
||||
*/
|
||||
function getGalleryUrl(photoId) {
|
||||
return PUBLIC_URL + '/gallery?expanded=photo-' + photoId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error state on an element
|
||||
*/
|
||||
function showError(el, message) {
|
||||
el.innerHTML =
|
||||
'<div class="photo-error">' +
|
||||
'<svg fill="currentColor" viewBox="0 0 20 20">' +
|
||||
'<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />' +
|
||||
'</svg>' +
|
||||
'<p>' + escapeHtml(message) + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch photo metadata from Media API
|
||||
*/
|
||||
async function fetchPhotoMetadata(photoId) {
|
||||
try {
|
||||
var response = await fetch(MEDIA_API_URL + '/api/public/photos/' + photoId);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch photo ' + photoId + ': ' + response.statusText);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching photo ' + photoId + ' metadata:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch album data from Media API
|
||||
*/
|
||||
async function fetchAlbumData(albumId) {
|
||||
try {
|
||||
var response = await fetch(MEDIA_API_URL + '/api/public/albums/' + albumId);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch album ' + albumId + ': ' + response.statusText);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching album ' + albumId + ':', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Photo Block ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a single photo block.
|
||||
* Replaces placeholder with an <img> tag, optional caption, and gallery link.
|
||||
*/
|
||||
async function renderPhotoBlock(el) {
|
||||
var photoId = parseInt(el.dataset.photoId, 10);
|
||||
var size = el.dataset.size || 'large';
|
||||
var caption = el.dataset.caption || '';
|
||||
var linkToGallery = el.dataset.linkToGallery !== 'false';
|
||||
var alignment = el.dataset.alignment || 'center';
|
||||
|
||||
if (!photoId || isNaN(photoId)) {
|
||||
showError(el, 'Invalid photo ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
el.innerHTML = '<div class="photo-loading">Loading photo...</div>';
|
||||
|
||||
// Fetch metadata for alt text and title
|
||||
var metadata = await fetchPhotoMetadata(photoId);
|
||||
if (!metadata) {
|
||||
showError(el, 'Photo not found (ID: ' + photoId + ')');
|
||||
return;
|
||||
}
|
||||
|
||||
var imgUrl = MEDIA_API_URL + '/api/public/photos/' + photoId + '/image?size=' + size;
|
||||
var altText = escapeHtml(metadata.title || 'Photo ' + photoId);
|
||||
|
||||
// Build image element
|
||||
var img = document.createElement('img');
|
||||
img.src = imgUrl;
|
||||
img.alt = altText;
|
||||
img.title = altText;
|
||||
img.style.width = '100%';
|
||||
img.style.height = 'auto';
|
||||
img.style.borderRadius = '8px';
|
||||
img.style.display = 'block';
|
||||
|
||||
// Clear and rebuild
|
||||
el.innerHTML = '';
|
||||
el.style.textAlign = alignment;
|
||||
|
||||
if (linkToGallery) {
|
||||
var link = document.createElement('a');
|
||||
link.href = getGalleryUrl(photoId);
|
||||
link.target = '_blank';
|
||||
link.appendChild(img);
|
||||
el.appendChild(link);
|
||||
} else {
|
||||
el.appendChild(img);
|
||||
}
|
||||
|
||||
// Add caption if provided (from data attribute or metadata)
|
||||
var captionText = caption || metadata.description || '';
|
||||
if (captionText) {
|
||||
var captionEl = document.createElement('div');
|
||||
captionEl.className = 'photo-caption';
|
||||
captionEl.textContent = captionText;
|
||||
el.appendChild(captionEl);
|
||||
}
|
||||
|
||||
console.log('Rendered photo block for photo ' + photoId);
|
||||
}
|
||||
|
||||
// ─── Photo Card ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Hydrate a photo card block:
|
||||
* 1. Fix thumbnail URL using MEDIA_API_URL
|
||||
* 2. Fetch live metadata (view count)
|
||||
* 3. Mark as hydrated
|
||||
*/
|
||||
async function hydratePhotoCard(cardEl) {
|
||||
var photoId = parseInt(cardEl.dataset.photoId, 10);
|
||||
if (!photoId || isNaN(photoId)) return;
|
||||
|
||||
// 1. Fix thumbnail URL
|
||||
var img = cardEl.querySelector('img');
|
||||
if (img) {
|
||||
var correctThumbUrl = MEDIA_API_URL + '/api/public/photos/' + photoId + '/thumbnail?v=' + Date.now();
|
||||
img.src = correctThumbUrl;
|
||||
img.onerror = function() {
|
||||
img.style.display = 'none';
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Fetch live metadata to update views
|
||||
try {
|
||||
var metadata = await fetchPhotoMetadata(photoId);
|
||||
if (metadata) {
|
||||
// Update view count if rendered in card
|
||||
var viewsEl = cardEl.querySelector('[data-role="views"]');
|
||||
if (viewsEl && metadata.viewCount !== undefined) {
|
||||
viewsEl.textContent = formatViewCount(metadata.viewCount);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-critical
|
||||
}
|
||||
|
||||
// 3. Fix "View →" link to use correct gallery URL
|
||||
var link = cardEl.querySelector('a');
|
||||
if (link) {
|
||||
link.href = getGalleryUrl(photoId);
|
||||
link.target = '_blank';
|
||||
}
|
||||
|
||||
// 4. Replace "View →" text with gallery button
|
||||
var infoBar = cardEl.querySelector('a > div:last-child');
|
||||
if (infoBar) {
|
||||
var flexRow = infoBar.querySelector('div:last-child');
|
||||
if (flexRow) {
|
||||
var spans = flexRow.querySelectorAll('span');
|
||||
var viewSpan = null;
|
||||
for (var i = 0; i < spans.length; i++) {
|
||||
if (spans[i].textContent.indexOf('View') !== -1) {
|
||||
viewSpan = spans[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (viewSpan) {
|
||||
var galleryLink = document.createElement('a');
|
||||
galleryLink.href = getGalleryUrl(photoId);
|
||||
galleryLink.target = '_blank';
|
||||
galleryLink.innerHTML = '📷 Gallery';
|
||||
galleryLink.className = 'photo-card-gallery-btn';
|
||||
galleryLink.style.cssText = 'background:#43cea2;border:none;color:#fff;font-size:12px;font-weight:600;padding:5px 14px;border-radius:4px;cursor:pointer;transition:background 0.15s;text-decoration:none;display:inline-block;';
|
||||
galleryLink.title = 'Open in gallery';
|
||||
galleryLink.onmouseenter = function() { galleryLink.style.background = '#5dd8b0'; };
|
||||
galleryLink.onmouseleave = function() { galleryLink.style.background = '#43cea2'; };
|
||||
galleryLink.addEventListener('click', function(ev) {
|
||||
ev.stopPropagation();
|
||||
});
|
||||
viewSpan.replaceWith(galleryLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as hydrated
|
||||
cardEl.dataset.hydrated = 'true';
|
||||
console.log('Hydrated photo card for photo ' + photoId);
|
||||
}
|
||||
|
||||
// ─── Photo Album ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a photo album block.
|
||||
* Fetches album data and builds a responsive grid of thumbnails.
|
||||
*/
|
||||
async function renderAlbumBlock(el) {
|
||||
var albumId = parseInt(el.dataset.albumId, 10);
|
||||
var columns = parseInt(el.dataset.columns, 10) || 3;
|
||||
var maxPhotos = parseInt(el.dataset.maxPhotos, 10) || 12;
|
||||
var showTitle = el.dataset.showTitle !== 'false';
|
||||
|
||||
if (!albumId || isNaN(albumId)) {
|
||||
showError(el, 'Invalid album ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
el.innerHTML = '<div class="photo-loading">Loading album...</div>';
|
||||
|
||||
// Fetch album data
|
||||
var album = await fetchAlbumData(albumId);
|
||||
if (!album) {
|
||||
showError(el, 'Album not found (ID: ' + albumId + ')');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build album content
|
||||
el.innerHTML = '';
|
||||
|
||||
// Album title
|
||||
if (showTitle && album.title) {
|
||||
var titleEl = document.createElement('h3');
|
||||
titleEl.className = 'photo-album-title';
|
||||
titleEl.textContent = album.title;
|
||||
el.appendChild(titleEl);
|
||||
}
|
||||
|
||||
// Photo grid
|
||||
var photos = (album.photos || []).slice(0, maxPhotos);
|
||||
if (photos.length === 0) {
|
||||
el.innerHTML += '<div class="photo-loading">This album has no photos yet.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var grid = document.createElement('div');
|
||||
grid.className = 'photo-album-grid cols-' + columns;
|
||||
|
||||
for (var i = 0; i < photos.length; i++) {
|
||||
var photo = photos[i];
|
||||
var cell = document.createElement('div');
|
||||
cell.className = 'photo-album-cell';
|
||||
|
||||
var cellLink = document.createElement('a');
|
||||
cellLink.href = getGalleryUrl(photo.id);
|
||||
cellLink.target = '_blank';
|
||||
cellLink.title = photo.title || 'Photo ' + photo.id;
|
||||
|
||||
var cellImg = document.createElement('img');
|
||||
cellImg.src = MEDIA_API_URL + '/api/public/photos/' + photo.id + '/thumbnail';
|
||||
cellImg.alt = photo.title || 'Photo ' + photo.id;
|
||||
cellImg.loading = 'lazy';
|
||||
|
||||
cellLink.appendChild(cellImg);
|
||||
cell.appendChild(cellLink);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
|
||||
el.appendChild(grid);
|
||||
|
||||
// "View full album" footer
|
||||
var footer = document.createElement('div');
|
||||
footer.className = 'photo-album-footer';
|
||||
var footerLink = document.createElement('a');
|
||||
footerLink.href = PUBLIC_URL + '/gallery?album=' + albumId;
|
||||
footerLink.target = '_blank';
|
||||
footerLink.textContent = 'View full album \u2192';
|
||||
footer.appendChild(footerLink);
|
||||
el.appendChild(footer);
|
||||
|
||||
console.log('Rendered album block for album ' + albumId + ' (' + photos.length + ' photos)');
|
||||
}
|
||||
|
||||
// ─── Photo Album Carousel ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a photo album carousel.
|
||||
* Fetches album data and builds a swipeable slideshow with navigation.
|
||||
*/
|
||||
async function renderCarouselBlock(el) {
|
||||
var albumId = parseInt(el.dataset.albumId, 10);
|
||||
var maxPhotos = parseInt(el.dataset.maxPhotos, 10) || 20;
|
||||
var showTitle = el.dataset.showTitle !== 'false';
|
||||
var autoPlayEnabled = el.dataset.autoPlay === 'true';
|
||||
|
||||
if (!albumId || isNaN(albumId)) {
|
||||
showError(el, 'Invalid album ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
el.innerHTML = '<div class="photo-loading">Loading carousel...</div>';
|
||||
|
||||
// Fetch album data
|
||||
var album = await fetchAlbumData(albumId);
|
||||
if (!album) {
|
||||
showError(el, 'Album not found (ID: ' + albumId + ')');
|
||||
return;
|
||||
}
|
||||
|
||||
var photos = (album.photos || []).slice(0, maxPhotos);
|
||||
if (photos.length === 0) {
|
||||
el.innerHTML = '<div class="photo-loading">This album has no photos yet.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build carousel
|
||||
el.innerHTML = '';
|
||||
el.classList.add('carousel-initialized');
|
||||
|
||||
// Album title
|
||||
if (showTitle && album.title) {
|
||||
var titleEl = document.createElement('h3');
|
||||
titleEl.className = 'photo-album-title';
|
||||
titleEl.textContent = album.title;
|
||||
el.appendChild(titleEl);
|
||||
}
|
||||
|
||||
// Viewport + track
|
||||
var viewport = document.createElement('div');
|
||||
viewport.className = 'carousel-viewport';
|
||||
|
||||
var track = document.createElement('div');
|
||||
track.className = 'carousel-track';
|
||||
|
||||
for (var i = 0; i < photos.length; i++) {
|
||||
var photo = photos[i];
|
||||
var slide = document.createElement('div');
|
||||
slide.className = 'carousel-slide';
|
||||
|
||||
var slideLink = document.createElement('a');
|
||||
slideLink.href = getGalleryUrl(photo.id);
|
||||
slideLink.target = '_blank';
|
||||
slideLink.title = photo.title || 'Photo ' + photo.id;
|
||||
|
||||
var slideImg = document.createElement('img');
|
||||
slideImg.src = MEDIA_API_URL + '/api/public/photos/' + photo.id + '/image?size=large';
|
||||
slideImg.alt = photo.title || 'Photo ' + photo.id;
|
||||
slideImg.loading = i === 0 ? 'eager' : 'lazy';
|
||||
|
||||
slideLink.appendChild(slideImg);
|
||||
slide.appendChild(slideLink);
|
||||
track.appendChild(slide);
|
||||
}
|
||||
|
||||
viewport.appendChild(track);
|
||||
el.appendChild(viewport);
|
||||
|
||||
// Navigation buttons (only if > 1 photo)
|
||||
if (photos.length > 1) {
|
||||
var prevBtn = document.createElement('button');
|
||||
prevBtn.className = 'carousel-btn carousel-btn-prev';
|
||||
prevBtn.setAttribute('aria-label', 'Previous photo');
|
||||
prevBtn.innerHTML = '❮';
|
||||
viewport.appendChild(prevBtn);
|
||||
|
||||
var nextBtn = document.createElement('button');
|
||||
nextBtn.className = 'carousel-btn carousel-btn-next';
|
||||
nextBtn.setAttribute('aria-label', 'Next photo');
|
||||
nextBtn.innerHTML = '❯';
|
||||
viewport.appendChild(nextBtn);
|
||||
|
||||
// Dot indicators
|
||||
var dotsWrap = document.createElement('div');
|
||||
dotsWrap.className = 'carousel-dots';
|
||||
for (var d = 0; d < photos.length; d++) {
|
||||
var dot = document.createElement('button');
|
||||
dot.className = 'carousel-dot' + (d === 0 ? ' active' : '');
|
||||
dot.setAttribute('aria-label', 'Go to photo ' + (d + 1));
|
||||
dot.dataset.index = String(d);
|
||||
dotsWrap.appendChild(dot);
|
||||
}
|
||||
el.appendChild(dotsWrap);
|
||||
|
||||
// Carousel state
|
||||
var currentIndex = 0;
|
||||
var totalSlides = photos.length;
|
||||
var autoPlayTimer = null;
|
||||
|
||||
function goTo(idx) {
|
||||
if (idx < 0) idx = totalSlides - 1;
|
||||
if (idx >= totalSlides) idx = 0;
|
||||
currentIndex = idx;
|
||||
track.style.transform = 'translateX(-' + (currentIndex * 100) + '%)';
|
||||
// Update dots
|
||||
var dots = dotsWrap.querySelectorAll('.carousel-dot');
|
||||
for (var j = 0; j < dots.length; j++) {
|
||||
dots[j].classList.toggle('active', j === currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
prevBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
goTo(currentIndex - 1);
|
||||
resetAutoPlay();
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
goTo(currentIndex + 1);
|
||||
resetAutoPlay();
|
||||
});
|
||||
|
||||
dotsWrap.addEventListener('click', function(e) {
|
||||
var target = e.target;
|
||||
if (target.classList.contains('carousel-dot')) {
|
||||
goTo(parseInt(target.dataset.index, 10));
|
||||
resetAutoPlay();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
goTo(currentIndex - 1);
|
||||
resetAutoPlay();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
goTo(currentIndex + 1);
|
||||
resetAutoPlay();
|
||||
}
|
||||
});
|
||||
|
||||
// Touch/swipe support
|
||||
var touchStartX = 0;
|
||||
var touchDeltaX = 0;
|
||||
viewport.addEventListener('touchstart', function(e) {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchDeltaX = 0;
|
||||
}, { passive: true });
|
||||
viewport.addEventListener('touchmove', function(e) {
|
||||
touchDeltaX = e.touches[0].clientX - touchStartX;
|
||||
}, { passive: true });
|
||||
viewport.addEventListener('touchend', function(e) {
|
||||
if (Math.abs(touchDeltaX) > 50) {
|
||||
e.preventDefault(); // suppress follow-up click from swipe
|
||||
if (touchDeltaX < 0) goTo(currentIndex + 1);
|
||||
else goTo(currentIndex - 1);
|
||||
resetAutoPlay();
|
||||
}
|
||||
touchDeltaX = 0;
|
||||
});
|
||||
|
||||
// Auto-play
|
||||
function startAutoPlay() {
|
||||
if (!autoPlayEnabled) return;
|
||||
stopAutoPlay();
|
||||
autoPlayTimer = setInterval(function() {
|
||||
goTo(currentIndex + 1);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopAutoPlay() {
|
||||
if (autoPlayTimer) {
|
||||
clearInterval(autoPlayTimer);
|
||||
autoPlayTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function resetAutoPlay() {
|
||||
stopAutoPlay();
|
||||
startAutoPlay();
|
||||
}
|
||||
|
||||
// Pause auto-play on hover
|
||||
el.addEventListener('mouseenter', stopAutoPlay);
|
||||
el.addEventListener('mouseleave', startAutoPlay);
|
||||
|
||||
startAutoPlay();
|
||||
}
|
||||
|
||||
// "View full album" footer
|
||||
var footer = document.createElement('div');
|
||||
footer.className = 'photo-album-footer';
|
||||
var footerLink = document.createElement('a');
|
||||
footerLink.href = PUBLIC_URL + '/gallery?album=' + albumId;
|
||||
footerLink.target = '_blank';
|
||||
footerLink.textContent = 'View full album \u2192';
|
||||
footer.appendChild(footerLink);
|
||||
el.appendChild(footer);
|
||||
|
||||
console.log('Rendered carousel for album ' + albumId + ' (' + photos.length + ' photos)');
|
||||
}
|
||||
|
||||
// ─── Initialization ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initialize all photo carousel blocks on the page
|
||||
*/
|
||||
function initPhotoCarousels() {
|
||||
var carousels = document.querySelectorAll('.photo-album-carousel');
|
||||
console.log('Found ' + carousels.length + ' photo carousel(s) to hydrate');
|
||||
carousels.forEach(function(carousel) {
|
||||
if (carousel.classList.contains('carousel-initialized')) return; // Already rendered
|
||||
renderCarouselBlock(carousel);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all photo blocks on the page
|
||||
*/
|
||||
function initPhotoBlocks() {
|
||||
var blocks = document.querySelectorAll('.photo-block');
|
||||
console.log('Found ' + blocks.length + ' photo block(s) to hydrate');
|
||||
blocks.forEach(function(block) {
|
||||
if (block.querySelector('img')) return; // Already rendered
|
||||
renderPhotoBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all photo card blocks on the page
|
||||
*/
|
||||
function initPhotoCards() {
|
||||
var cards = document.querySelectorAll('.photo-card-block');
|
||||
console.log('Found ' + cards.length + ' photo card(s) to hydrate');
|
||||
cards.forEach(function(card) {
|
||||
if (card.dataset.hydrated === 'true') return;
|
||||
hydratePhotoCard(card);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all photo album blocks on the page
|
||||
*/
|
||||
function initPhotoAlbums() {
|
||||
var albums = document.querySelectorAll('.photo-album-block');
|
||||
console.log('Found ' + albums.length + ' photo album(s) to hydrate');
|
||||
albums.forEach(function(album) {
|
||||
if (album.querySelector('.photo-album-grid')) return; // Already rendered
|
||||
renderAlbumBlock(album);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all photo elements (blocks + cards + albums + carousels)
|
||||
*/
|
||||
function initAll() {
|
||||
initPhotoBlocks();
|
||||
initPhotoCards();
|
||||
initPhotoAlbums();
|
||||
initPhotoCarousels();
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initAll);
|
||||
} else {
|
||||
initAll();
|
||||
}
|
||||
|
||||
// Re-initialize on MkDocs SPA navigation
|
||||
if (typeof window.document$ !== 'undefined') {
|
||||
window.document$.subscribe(function() {
|
||||
console.log('MkDocs navigation detected, re-initializing photo elements');
|
||||
setTimeout(initAll, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -7,10 +7,10 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"open_issues_count": 23,
|
||||
"updated_at": "2026-02-18T10:02:02-07:00",
|
||||
"updated_at": "2026-02-21T11:52:28-07:00",
|
||||
"created_at": "2025-05-28T14:54:59-06:00",
|
||||
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
|
||||
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
|
||||
"default_branch": "main",
|
||||
"last_build_update": "2026-02-18T10:02:02-07:00"
|
||||
"last_build_update": "2026-02-21T11:52:28-07:00"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
|
||||
"html_url": "https://github.com/anthropics/claude-code",
|
||||
"language": "Shell",
|
||||
"stars_count": 67570,
|
||||
"forks_count": 5278,
|
||||
"open_issues_count": 6297,
|
||||
"updated_at": "2026-02-18T18:41:22Z",
|
||||
"stars_count": 70476,
|
||||
"forks_count": 5568,
|
||||
"open_issues_count": 6924,
|
||||
"updated_at": "2026-02-26T14:25:29Z",
|
||||
"created_at": "2025-02-22T17:41:21Z",
|
||||
"clone_url": "https://github.com/anthropics/claude-code.git",
|
||||
"ssh_url": "git@github.com:anthropics/claude-code.git",
|
||||
"default_branch": "main",
|
||||
"last_build_update": "2026-02-17T18:53:52Z"
|
||||
"last_build_update": "2026-02-26T12:05:37Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "VS Code in the browser",
|
||||
"html_url": "https://github.com/coder/code-server",
|
||||
"language": "TypeScript",
|
||||
"stars_count": 76286,
|
||||
"forks_count": 6513,
|
||||
"open_issues_count": 177,
|
||||
"updated_at": "2026-02-18T17:58:26Z",
|
||||
"stars_count": 76383,
|
||||
"forks_count": 6527,
|
||||
"open_issues_count": 184,
|
||||
"updated_at": "2026-02-26T13:37:54Z",
|
||||
"created_at": "2019-02-27T16:50:41Z",
|
||||
"clone_url": "https://github.com/coder/code-server.git",
|
||||
"ssh_url": "git@github.com:coder/code-server.git",
|
||||
"default_branch": "main",
|
||||
"last_build_update": "2026-02-14T12:44:57Z"
|
||||
"last_build_update": "2026-02-25T22:36:16Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
|
||||
"html_url": "https://github.com/gethomepage/homepage",
|
||||
"language": "JavaScript",
|
||||
"stars_count": 28459,
|
||||
"forks_count": 1789,
|
||||
"stars_count": 28611,
|
||||
"forks_count": 1795,
|
||||
"open_issues_count": 1,
|
||||
"updated_at": "2026-02-18T17:08:54Z",
|
||||
"updated_at": "2026-02-26T12:19:48Z",
|
||||
"created_at": "2022-08-24T07:29:42Z",
|
||||
"clone_url": "https://github.com/gethomepage/homepage.git",
|
||||
"ssh_url": "git@github.com:gethomepage/homepage.git",
|
||||
"default_branch": "dev",
|
||||
"last_build_update": "2026-02-18T12:23:05Z"
|
||||
"last_build_update": "2026-02-26T12:25:33Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
|
||||
"html_url": "https://github.com/go-gitea/gitea",
|
||||
"language": "Go",
|
||||
"stars_count": 53764,
|
||||
"forks_count": 6394,
|
||||
"open_issues_count": 2832,
|
||||
"updated_at": "2026-02-18T18:54:21Z",
|
||||
"stars_count": 53910,
|
||||
"forks_count": 6399,
|
||||
"open_issues_count": 2843,
|
||||
"updated_at": "2026-02-26T13:59:20Z",
|
||||
"created_at": "2016-11-01T02:13:26Z",
|
||||
"clone_url": "https://github.com/go-gitea/gitea.git",
|
||||
"ssh_url": "git@github.com:go-gitea/gitea.git",
|
||||
"default_branch": "main",
|
||||
"last_build_update": "2026-02-18T07:31:28Z"
|
||||
"last_build_update": "2026-02-26T11:57:03Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
|
||||
"html_url": "https://github.com/knadh/listmonk",
|
||||
"language": "Go",
|
||||
"stars_count": 19078,
|
||||
"forks_count": 1928,
|
||||
"stars_count": 19131,
|
||||
"forks_count": 1936,
|
||||
"open_issues_count": 113,
|
||||
"updated_at": "2026-02-18T10:42:44Z",
|
||||
"updated_at": "2026-02-26T12:29:54Z",
|
||||
"created_at": "2019-06-26T05:08:39Z",
|
||||
"clone_url": "https://github.com/knadh/listmonk.git",
|
||||
"ssh_url": "git@github.com:knadh/listmonk.git",
|
||||
"default_branch": "master",
|
||||
"last_build_update": "2026-02-17T05:10:24Z"
|
||||
"last_build_update": "2026-02-26T13:26:07Z"
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
|
||||
"html_url": "https://github.com/lyqht/mini-qr",
|
||||
"language": "Vue",
|
||||
"stars_count": 1852,
|
||||
"forks_count": 236,
|
||||
"open_issues_count": 21,
|
||||
"updated_at": "2026-02-17T22:05:48Z",
|
||||
"stars_count": 1878,
|
||||
"forks_count": 240,
|
||||
"open_issues_count": 24,
|
||||
"updated_at": "2026-02-26T06:04:32Z",
|
||||
"created_at": "2023-04-21T14:20:14Z",
|
||||
"clone_url": "https://github.com/lyqht/mini-qr.git",
|
||||
"ssh_url": "git@github.com:lyqht/mini-qr.git",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
|
||||
"html_url": "https://github.com/n8n-io/n8n",
|
||||
"language": "TypeScript",
|
||||
"stars_count": 175133,
|
||||
"forks_count": 54959,
|
||||
"open_issues_count": 1379,
|
||||
"updated_at": "2026-02-18T18:44:06Z",
|
||||
"stars_count": 176496,
|
||||
"forks_count": 55212,
|
||||
"open_issues_count": 1408,
|
||||
"updated_at": "2026-02-26T14:25:19Z",
|
||||
"created_at": "2019-06-22T09:24:21Z",
|
||||
"clone_url": "https://github.com/n8n-io/n8n.git",
|
||||
"ssh_url": "git@github.com:n8n-io/n8n.git",
|
||||
"default_branch": "master",
|
||||
"last_build_update": "2026-02-18T18:45:01Z"
|
||||
"last_build_update": "2026-02-26T14:25:24Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
|
||||
"html_url": "https://github.com/nocodb/nocodb",
|
||||
"language": "TypeScript",
|
||||
"stars_count": 62055,
|
||||
"forks_count": 4637,
|
||||
"open_issues_count": 595,
|
||||
"updated_at": "2026-02-18T18:47:12Z",
|
||||
"stars_count": 62217,
|
||||
"forks_count": 4647,
|
||||
"open_issues_count": 602,
|
||||
"updated_at": "2026-02-25T22:03:40Z",
|
||||
"created_at": "2017-10-29T18:51:48Z",
|
||||
"clone_url": "https://github.com/nocodb/nocodb.git",
|
||||
"ssh_url": "git@github.com:nocodb/nocodb.git",
|
||||
"default_branch": "develop",
|
||||
"last_build_update": "2026-02-18T17:46:29Z"
|
||||
"last_build_update": "2026-02-25T19:01:33Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
|
||||
"html_url": "https://github.com/ollama/ollama",
|
||||
"language": "Go",
|
||||
"stars_count": 162853,
|
||||
"forks_count": 14611,
|
||||
"open_issues_count": 2427,
|
||||
"updated_at": "2026-02-18T18:55:27Z",
|
||||
"stars_count": 163469,
|
||||
"forks_count": 14678,
|
||||
"open_issues_count": 2486,
|
||||
"updated_at": "2026-02-26T14:21:37Z",
|
||||
"created_at": "2023-06-26T19:39:32Z",
|
||||
"clone_url": "https://github.com/ollama/ollama.git",
|
||||
"ssh_url": "git@github.com:ollama/ollama.git",
|
||||
"default_branch": "main",
|
||||
"last_build_update": "2026-02-18T02:18:06Z"
|
||||
"last_build_update": "2026-02-26T03:28:54Z"
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
"description": "Documentation that simply works",
|
||||
"html_url": "https://github.com/squidfunk/mkdocs-material",
|
||||
"language": "Python",
|
||||
"stars_count": 26074,
|
||||
"forks_count": 4044,
|
||||
"stars_count": 26122,
|
||||
"forks_count": 4047,
|
||||
"open_issues_count": 2,
|
||||
"updated_at": "2026-02-18T16:48:45Z",
|
||||
"updated_at": "2026-02-26T12:52:29Z",
|
||||
"created_at": "2016-01-28T22:09:23Z",
|
||||
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
|
||||
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
|
||||
"default_branch": "master",
|
||||
"last_build_update": "2026-02-18T15:52:24Z"
|
||||
"last_build_update": "2026-02-24T12:06:19Z"
|
||||
}
|
||||
Reference in New Issue
Block a user