capitalize "close" and "cancel" in translation strings

feat(gallery): improve image loading performance
This commit is contained in:
lzinga 2025-11-28 15:01:25 -08:00
parent 6512d03c11
commit 4779492bb7
3 changed files with 99 additions and 83 deletions

View File

@ -2131,10 +2131,10 @@
"delete": "Delete", "delete": "Delete",
"previous": "Previous", "previous": "Previous",
"next": "Next", "next": "Next",
"close": "close", "close": "Close",
"goto_note": "Go To Parent Note", "goto_note": "Go To Parent Note",
"warning_delete_other_note": "This image belongs to another note, deleting it will remove it from that 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", "copy_image_link": "Image Link Copied to Clipboard",
"share_copy_failed": "Failed to copy image link to clipboard.", "share_copy_failed": "Failed to copy image link to clipboard.",
"select_image": "Select Image", "select_image": "Select Image",

View File

@ -126,7 +126,7 @@
.gallery-empty-hint { .gallery-empty-hint {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 1.5rem !important; margin-bottom: 1.5rem;
} }
/* Lightbox */ /* Lightbox */
@ -256,6 +256,7 @@
left: 0.5rem; left: 0.5rem;
z-index: 10; z-index: 10;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
border-radius: 4px; border-radius: 4px;
padding: 0.5rem; padding: 0.5rem;
@ -279,6 +280,24 @@
gap: 0.5rem; 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 { .gallery-item-delete {
background: rgba(220, 53, 69, 0.9); background: rgba(220, 53, 69, 0.9);
color: white; color: white;
@ -320,6 +339,29 @@
background: rgba(220, 53, 69, 1); 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 */ /* Toolbar Styles */
.gallery-toolbar-left { .gallery-toolbar-left {
display: flex; display: flex;
@ -339,6 +381,7 @@
.btn-danger:hover:not(:disabled) { .btn-danger:hover:not(:disabled) {
background: rgba(220, 53, 69, 0.2); background: rgba(220, 53, 69, 0.2);
} }
.gallery-item-link { .gallery-item-link {
position: absolute; position: absolute;
bottom: 0.75rem; bottom: 0.75rem;

View File

@ -51,6 +51,7 @@ export default function Gallery({ note }: TypeWidgetProps) {
const directAttachments = await note.getAttachments(); const directAttachments = await note.getAttachments();
const directImageAttachments = directAttachments.filter(a => a.role === "image"); const directImageAttachments = directAttachments.filter(a => a.role === "image");
// Process direct attachments
for (const attachment of directImageAttachments) { for (const attachment of directImageAttachments) {
const size = attachment.contentLength || 0; const size = attachment.contentLength || 0;
calculatedTotalSize += size; calculatedTotalSize += size;
@ -71,40 +72,57 @@ export default function Gallery({ note }: TypeWidgetProps) {
const childNotes = await note.getChildNotes(); const childNotes = await note.getChildNotes();
const imageNotes = childNotes.filter(n => n.type === "image"); const imageNotes = childNotes.filter(n => n.type === "image");
// Convert image notes to ImageItem format // Process image notes in parallel
for (const imageNote of imageNotes) { const imageBlobPromises = imageNotes.map(async (imageNote) => {
const blob = await imageNote.getBlob(); const blob = await imageNote.getBlob();
const size = blob?.contentLength || 0; const size = blob?.contentLength || 0;
calculatedTotalSize += size; calculatedTotalSize += size;
imageItems.push({ return {
id: imageNote.noteId, id: imageNote.noteId,
url: `api/images/${imageNote.noteId}/${encodeURIComponent(imageNote.title)}`, url: `api/images/${imageNote.noteId}/${encodeURIComponent(imageNote.title)}`,
title: imageNote.title, title: imageNote.title,
type: 'note' as const, type: 'note' as const,
size size
}); };
} });
// Also check for notes with image attachments const imageNoteItems = await Promise.all(imageBlobPromises);
for (const childNote of childNotes) { 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 attachments = await childNote.getAttachments();
const imageAttachments = attachments.filter(a => a.role === "image"); const imageAttachments = attachments.filter(a => a.role === "image");
for (const attachment of imageAttachments) { return imageAttachments.map(attachment => {
const size = attachment.contentLength || 0; const size = attachment.contentLength || 0;
calculatedTotalSize += size;
imageItems.push({ return {
id: attachment.attachmentId, id: attachment.attachmentId,
url: `api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}`, url: `api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}`,
title: attachment.title, title: attachment.title,
type: 'attachment' as const, type: 'attachment' as const,
noteId: attachment.ownerId, noteId: attachment.ownerId,
size 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); setImages(imageItems);
@ -118,32 +136,14 @@ export default function Gallery({ note }: TypeWidgetProps) {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => { useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const childNoteIds = images.map(img => img.noteId).filter(Boolean); const childNoteIds = images.map(img => img.noteId).filter(Boolean);
// Reload if branches change (child notes added/removed or share status changed) const shouldReload =
const branchRows = loadResults.getBranchRows(); loadResults.getBranchRows().some(b => b.parentNoteId === note.noteId || b.noteId === note.noteId) ||
if (branchRows.some(b => b.parentNoteId === note.noteId || b.noteId === note.noteId)) { loadResults.getNoteIds().some(id => id === note.noteId || childNoteIds.includes(id)) ||
loadImages(); loadResults.getAttachmentRows().some(att => att.ownerId === note.noteId || childNoteIds.includes(att.ownerId)) ||
return; loadResults.getAttributeRows().some(attr => attr.noteId === note.noteId && attr.name === "hideChildAttachments");
}
// Reload if any child note changes if (shouldReload) {
const noteIds = loadResults.getNoteIds();
if (noteIds.some(id => id === note.noteId || childNoteIds.includes(id))) {
loadImages(); 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 // 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 // Your current handleCopyImageLink is actually fine for both Electron and web
async function handleCopyImageLink(img: ImageItem, e: MouseEvent) { async function handleCopyImageLink(img: ImageItem, e: MouseEvent) {
e.stopPropagation(); e.stopPropagation();
@ -245,20 +227,6 @@ export default function Gallery({ note }: TypeWidgetProps) {
await loadImages(); 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 { function getAbsoluteUrl(path: string): string {
if (syncServerHost) { if (syncServerHost) {
return new URL(path, syncServerHost).href; 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 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 successCount = 0;
let errorCount = 0; let errorCount = 0;
for (const img of imagesToDelete) { results.forEach((result, index) => {
try { if (result.status === 'fulfilled') {
if (img.type === 'note') {
await server.remove(`notes/${img.id}`);
} else {
await server.remove(`attachments/${img.id}`);
}
successCount++; successCount++;
} catch (error) { } else {
console.error(`Failed to delete image ${img.id}:`, error);
errorCount++; errorCount++;
console.error(`Failed to delete image ${imagesToDelete[index].id}:`, result.reason);
} }
} });
if (successCount > 0) { if (successCount > 0) {
toast.showMessage(t("gallery.delete_multiple_success", { count: successCount })); toast.showMessage(t("gallery.delete_multiple_success", { count: successCount }));
@ -483,7 +457,6 @@ export default function Gallery({ note }: TypeWidgetProps) {
<span className="bx bx-image-alt"></span> <span className="bx bx-image-alt"></span>
</div> </div>
<p>{t("gallery.no_images")}</p> <p>{t("gallery.no_images")}</p>
<p className="gallery-empty-hint">{t("gallery.upload_hint")}</p>
<Button <Button
icon="bx bx-upload" icon="bx bx-upload"
text={t("gallery.upload_first_image")} text={t("gallery.upload_first_image")}