From 4779492bb7f6a196b5cab91d1063d6aaeab56abe Mon Sep 17 00:00:00 2001 From: lzinga Date: Fri, 28 Nov 2025 15:01:25 -0800 Subject: [PATCH] capitalize "close" and "cancel" in translation strings feat(gallery): improve image loading performance --- .../src/translations/en/translation.json | 4 +- .../src/widgets/type_widgets/Gallery.css | 45 +++++- .../src/widgets/type_widgets/Gallery.tsx | 133 +++++++----------- 3 files changed, 99 insertions(+), 83 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 7ea66d3f7..e478e8a92 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2131,10 +2131,10 @@ "delete": "Delete", "previous": "Previous", "next": "Next", - "close": "close", + "close": "Close", "goto_note": "Go To Parent Note", "warning_delete_other_note": "This image belongs to another note, deleting it will remove it from that note!", - "cancel": "cancel", + "cancel": "Cancel", "copy_image_link": "Image Link Copied to Clipboard", "share_copy_failed": "Failed to copy image link to clipboard.", "select_image": "Select Image", diff --git a/apps/client/src/widgets/type_widgets/Gallery.css b/apps/client/src/widgets/type_widgets/Gallery.css index c09b0ff49..557a184cd 100644 --- a/apps/client/src/widgets/type_widgets/Gallery.css +++ b/apps/client/src/widgets/type_widgets/Gallery.css @@ -126,7 +126,7 @@ .gallery-empty-hint { font-size: 0.875rem; - margin-bottom: 1.5rem !important; + margin-bottom: 1.5rem; } /* Lightbox */ @@ -256,6 +256,7 @@ left: 0.5rem; z-index: 10; background: rgba(0, 0, 0, 0.7); + -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); border-radius: 4px; padding: 0.5rem; @@ -279,6 +280,24 @@ gap: 0.5rem; } +.gallery-item-copy-link { + background: rgba(13, 110, 253, 0.9); + color: white; + border: none; + border-radius: 4px; + padding: 0.375rem 0.5rem; + cursor: pointer; + font-size: 1.125rem; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; +} + +.gallery-item-copy-link:hover { + background: rgba(13, 110, 253, 1); +} + .gallery-item-delete { background: rgba(220, 53, 69, 0.9); color: white; @@ -320,6 +339,29 @@ background: rgba(220, 53, 69, 1); } +.gallery-lightbox-copy-link { + position: absolute; + top: 1rem; + right: 9rem; + background: rgba(13, 110, 253, 0.9); + border: none; + color: white; + font-size: 1.5rem; + width: 3rem; + height: 3rem; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; + z-index: 10001; +} + +.gallery-lightbox-copy-link:hover { + background: rgba(13, 110, 253, 1); +} + /* Toolbar Styles */ .gallery-toolbar-left { display: flex; @@ -339,6 +381,7 @@ .btn-danger:hover:not(:disabled) { background: rgba(220, 53, 69, 0.2); } + .gallery-item-link { position: absolute; bottom: 0.75rem; diff --git a/apps/client/src/widgets/type_widgets/Gallery.tsx b/apps/client/src/widgets/type_widgets/Gallery.tsx index 9faf42347..1cf6223c1 100644 --- a/apps/client/src/widgets/type_widgets/Gallery.tsx +++ b/apps/client/src/widgets/type_widgets/Gallery.tsx @@ -51,6 +51,7 @@ export default function Gallery({ note }: TypeWidgetProps) { const directAttachments = await note.getAttachments(); const directImageAttachments = directAttachments.filter(a => a.role === "image"); + // Process direct attachments for (const attachment of directImageAttachments) { const size = attachment.contentLength || 0; calculatedTotalSize += size; @@ -71,40 +72,57 @@ export default function Gallery({ note }: TypeWidgetProps) { const childNotes = await note.getChildNotes(); const imageNotes = childNotes.filter(n => n.type === "image"); - // Convert image notes to ImageItem format - for (const imageNote of imageNotes) { + // Process image notes in parallel + const imageBlobPromises = imageNotes.map(async (imageNote) => { const blob = await imageNote.getBlob(); const size = blob?.contentLength || 0; calculatedTotalSize += size; - imageItems.push({ + return { id: imageNote.noteId, url: `api/images/${imageNote.noteId}/${encodeURIComponent(imageNote.title)}`, title: imageNote.title, type: 'note' as const, size - }); - } + }; + }); - // Also check for notes with image attachments - for (const childNote of childNotes) { + const imageNoteItems = await Promise.all(imageBlobPromises); + imageItems.push(...imageNoteItems); + + // Recalculate total size from image note items + imageNoteItems.forEach(item => { + calculatedTotalSize += item.size || 0; + }); + + // Process child note attachments in parallel + const attachmentPromises = childNotes.map(async (childNote) => { const attachments = await childNote.getAttachments(); const imageAttachments = attachments.filter(a => a.role === "image"); - for (const attachment of imageAttachments) { + return imageAttachments.map(attachment => { const size = attachment.contentLength || 0; - calculatedTotalSize += size; - imageItems.push({ + return { id: attachment.attachmentId, url: `api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}`, title: attachment.title, type: 'attachment' as const, noteId: attachment.ownerId, size - }); - } - } + }; + }); + }); + + const attachmentArrays = await Promise.all(attachmentPromises); + const allAttachments = attachmentArrays.flat(); + + imageItems.push(...allAttachments); + + // Calculate total size from attachments + allAttachments.forEach(item => { + calculatedTotalSize += item.size || 0; + }); } setImages(imageItems); @@ -118,32 +136,14 @@ export default function Gallery({ note }: TypeWidgetProps) { useTriliumEvent("entitiesReloaded", ({ loadResults }) => { const childNoteIds = images.map(img => img.noteId).filter(Boolean); - // Reload if branches change (child notes added/removed or share status changed) - const branchRows = loadResults.getBranchRows(); - if (branchRows.some(b => b.parentNoteId === note.noteId || b.noteId === note.noteId)) { - loadImages(); - return; - } + const shouldReload = + loadResults.getBranchRows().some(b => b.parentNoteId === note.noteId || b.noteId === note.noteId) || + loadResults.getNoteIds().some(id => id === note.noteId || childNoteIds.includes(id)) || + loadResults.getAttachmentRows().some(att => att.ownerId === note.noteId || childNoteIds.includes(att.ownerId)) || + loadResults.getAttributeRows().some(attr => attr.noteId === note.noteId && attr.name === "hideChildAttachments"); - // Reload if any child note changes - const noteIds = loadResults.getNoteIds(); - if (noteIds.some(id => id === note.noteId || childNoteIds.includes(id))) { + if (shouldReload) { loadImages(); - return; - } - - // Reload if attachments change for this note or its children - const attachmentRows = loadResults.getAttachmentRows(); - if (attachmentRows.some(att => att.ownerId === note.noteId || childNoteIds.includes(att.ownerId))) { - loadImages(); - return; - } - - // Reload if attributes change (for hideChildImages label) - const attributeRows = loadResults.getAttributeRows(); - if (attributeRows.some(attr => attr.noteId === note.noteId && attr.name === "hideChildAttachments")) { - loadImages(); - return; } }); @@ -175,24 +175,6 @@ export default function Gallery({ note }: TypeWidgetProps) { // Note: No else clause - we don't do anything for gallery note attachments } - async function handleToggleShare() { - if (isGalleryShared) { - const shareBranch = note.getParentBranches().find((b) => b.parentNoteId === "_share"); - if (shareBranch?.branchId) { - await server.remove(`branches/${shareBranch.branchId}`); - toast.showMessage(t("gallery.unshared_success")); - sync.syncNow(true); - await loadImages(); - } - } else { - // Share the gallery - await branches.cloneNoteToParentNote(note.noteId, "_share"); - toast.showMessage(t("gallery.shared_success")); - sync.syncNow(true); - await loadImages(); - } - } - // Your current handleCopyImageLink is actually fine for both Electron and web async function handleCopyImageLink(img: ImageItem, e: MouseEvent) { e.stopPropagation(); @@ -245,20 +227,6 @@ export default function Gallery({ note }: TypeWidgetProps) { await loadImages(); } - function getShareUrl(noteId: string, syncServerHost: string | null): string | null { - const shareId = note.getOwnedLabelValue("shareAlias") || noteId; - - if (syncServerHost) { - return new URL(`/share/${shareId}`, syncServerHost).href; - } else { - let host = location.host; - if (host.endsWith("/")) { - host = host.substring(0, host.length - 1); - } - return `${location.protocol}//${host}${location.pathname}share/${shareId}`; - } - } - function getAbsoluteUrl(path: string): string { if (syncServerHost) { return new URL(path, syncServerHost).href; @@ -369,22 +337,28 @@ export default function Gallery({ note }: TypeWidgetProps) { } const imagesToDelete = images.filter(img => selectedImages.has(img.id)); + + const deletePromises = imagesToDelete.map(img => { + if (img.type === 'note') { + return server.remove(`notes/${img.id}`); + } else { + return server.remove(`attachments/${img.id}`); + } + }); + + const results = await Promise.allSettled(deletePromises); + let successCount = 0; let errorCount = 0; - for (const img of imagesToDelete) { - try { - if (img.type === 'note') { - await server.remove(`notes/${img.id}`); - } else { - await server.remove(`attachments/${img.id}`); - } + results.forEach((result, index) => { + if (result.status === 'fulfilled') { successCount++; - } catch (error) { - console.error(`Failed to delete image ${img.id}:`, error); + } else { errorCount++; + console.error(`Failed to delete image ${imagesToDelete[index].id}:`, result.reason); } - } + }); if (successCount > 0) { toast.showMessage(t("gallery.delete_multiple_success", { count: successCount })); @@ -483,7 +457,6 @@ export default function Gallery({ note }: TypeWidgetProps) {

{t("gallery.no_images")}

-

{t("gallery.upload_hint")}