This commit is contained in:
th committed 2026-04-07 21:58:45 +02:00
1 parent a9603aa0e8
commit 719f9d51ff
13 files changed
+625 -70

No files matched your search

+80 -3
View File
@@ -1,7 +1,7 @@
// ── Detail view ──
import { $, esc, api, fuzzyMatch } from './util.js';
import { state, getChildren, getGroupCards, getRecent, markRecent } from './state.js';
import { state, parseTags, tagChipHtml, getChildren, getGroupCards, getRecent, markRecent } from './state.js';
import { renderMd, stripMd, firstImage, addCodeCopyButtons } from './markdown.js';
import { pushCardDelete, pushCardEdit, snapshotCard } from './undo.js';
import { showDialog } from './dialog.js';
@@ -87,6 +87,9 @@ function clearMorphGhost() {
}
}
let galleryImages = [];
let galleryIndex = 0;
function ensureImageFullscreenOverlay() {
if (imageFsOverlay) return imageFsOverlay;
@@ -96,6 +99,9 @@ function ensureImageFullscreenOverlay() {
<button type="button" class="detail-image-fs-close" aria-label="close image">
<i class="ph ph-x text-xl"></i>
</button>
<button type="button" class="detail-image-fs-nav detail-image-fs-prev" aria-label="previous"><i class="ph ph-caret-left"></i></button>
<button type="button" class="detail-image-fs-nav detail-image-fs-next" aria-label="next"><i class="ph ph-caret-right"></i></button>
<div class="detail-image-fs-counter"></div>
<div class="detail-image-fs-stage">
<img class="detail-image-fs-img" alt="">
</div>
@@ -104,6 +110,8 @@ function ensureImageFullscreenOverlay() {
if (e.target === overlay) closeImageFullscreen();
});
overlay.querySelector('.detail-image-fs-close')?.addEventListener('click', closeImageFullscreen);
overlay.querySelector('.detail-image-fs-prev')?.addEventListener('click', e => { e.stopPropagation(); navigateGallery(-1); });
overlay.querySelector('.detail-image-fs-next')?.addEventListener('click', e => { e.stopPropagation(); navigateGallery(1); });
document.body.appendChild(overlay);
imageFsOverlay = overlay;
@@ -118,6 +126,26 @@ function ensureImageFullscreenOverlay() {
return overlay;
}
function updateGalleryNav() {
if (!imageFsOverlay) return;
const hasGallery = galleryImages.length > 1;
imageFsOverlay.querySelector('.detail-image-fs-prev').style.display = hasGallery ? '' : 'none';
imageFsOverlay.querySelector('.detail-image-fs-next').style.display = hasGallery ? '' : 'none';
const counter = imageFsOverlay.querySelector('.detail-image-fs-counter');
counter.style.display = hasGallery ? '' : 'none';
if (hasGallery) counter.textContent = `${galleryIndex + 1} / ${galleryImages.length}`;
}
function navigateGallery(dir) {
if (galleryImages.length < 2) return;
galleryIndex = (galleryIndex + dir + galleryImages.length) % galleryImages.length;
resetImageFsTransform();
if (imageFsImg) {
imageFsImg.src = galleryImages[galleryIndex];
}
updateGalleryNav();
}
function applyImageFsTransform() {
if (!imageFsImg) return;
imageFsImg.style.transform = `translate(${fsTx}px, ${fsTy}px) scale(${fsScale})`;
@@ -178,14 +206,18 @@ function onImageFsMouseUp() {
imageFsOverlay?.classList.remove('dragging');
}
function openImageFullscreen(src, alt = '') {
function openImageFullscreen(src, alt = '', gallery = []) {
if (!src) return;
const overlay = ensureImageFullscreenOverlay();
resetImageFsTransform();
galleryImages = gallery.length > 1 ? gallery : [];
galleryIndex = galleryImages.indexOf(src);
if (galleryIndex < 0) galleryIndex = 0;
if (imageFsImg) {
imageFsImg.src = src;
imageFsImg.alt = alt || 'image';
}
updateGalleryNav();
overlay.classList.add('open');
document.body.classList.add('detail-image-fs-open');
}
@@ -433,6 +465,32 @@ function showContent(card) {
(card.bucket_name ? ' | bucket: ' + card.bucket_name : '');
applyZoom();
// Tags
const tags = parseTags(card);
const tagEl = $('detail-tags');
if (tagEl) {
if (tags.length) {
tagEl.classList.remove('hidden');
tagEl.innerHTML = tags.map(t => tagChipHtml(t)).join('');
tagEl.querySelectorAll('.tag-seg[data-tag]').forEach(seg => {
seg.addEventListener('click', e => {
const tag = seg.dataset.tag;
if (e.ctrlKey || e.metaKey) {
const idx = state.activeTags.indexOf(tag);
if (idx >= 0) state.activeTags.splice(idx, 1);
else state.activeTags.push(tag);
} else {
state.activeTags = [tag];
}
window._renderTagBar?.();
window._renderBoard();
});
});
} else {
tagEl.classList.add('hidden');
}
}
// Backlinks
const pattern = `[[${card.title}]]`.toLowerCase();
const backlinks = state.cards.filter(c => c.id != card.id && (c.body || '').toLowerCase().includes(pattern));
@@ -505,7 +563,24 @@ function applyReadingResizeHandles(root) {
function bindDetailImageFullscreen(root) {
if (!root) return;
// Gallery grids: single click, with prev/next
root.querySelectorAll('.md-gallery').forEach(gallery => {
const srcs = Array.from(gallery.querySelectorAll('img')).map(i => i.getAttribute('src'));
gallery.querySelectorAll('.md-gallery-item').forEach(item => {
const img = item.querySelector('img');
if (!img) return;
item.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
openImageFullscreen(img.getAttribute('src'), img.getAttribute('alt') || 'image', srcs);
});
});
});
// Regular images: double-click, no gallery nav
root.querySelectorAll('img').forEach(img => {
if (img.closest('.md-gallery')) return;
img.title = 'double-click to open fullscreen';
img.addEventListener('dblclick', e => {
e.preventDefault();
@@ -526,8 +601,10 @@ detail.addEventListener('click', e => {
$('detail-close')?.addEventListener('click', closeDetail);
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (!imageFsOverlay?.classList.contains('open')) return;
if (e.key === 'ArrowLeft') { navigateGallery(-1); e.preventDefault(); return; }
if (e.key === 'ArrowRight') { navigateGallery(1); e.preventDefault(); return; }
if (e.key !== 'Escape') return;
e.preventDefault();
e.stopImmediatePropagation();
closeImageFullscreen();