mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 15:04:24 +01:00
Merge 0a860ec963099dc3784b05536be25ce9b140da50 into b4ab07bd78871f3886cf0291279d2f2715a73c24
This commit is contained in:
commit
946eced23e
@ -27,7 +27,8 @@ const NOTE_TYPE_ICONS = {
|
|||||||
doc: "bx bxs-file-doc",
|
doc: "bx bxs-file-doc",
|
||||||
contentWidget: "bx bxs-widget",
|
contentWidget: "bx bxs-widget",
|
||||||
mindMap: "bx bx-sitemap",
|
mindMap: "bx bx-sitemap",
|
||||||
aiChat: "bx bx-bot"
|
aiChat: "bx bx-bot",
|
||||||
|
gallery: "bx bx-image-alt"
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +36,7 @@ const NOTE_TYPE_ICONS = {
|
|||||||
* end user. Those types should be used only for checking against, they are
|
* end user. Those types should be used only for checking against, they are
|
||||||
* not for direct use.
|
* not for direct use.
|
||||||
*/
|
*/
|
||||||
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat";
|
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat" | "gallery";
|
||||||
|
|
||||||
export interface NotePathRecord {
|
export interface NotePathRecord {
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
|
|||||||
@ -18,7 +18,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
|||||||
search: null,
|
search: null,
|
||||||
text: null,
|
text: null,
|
||||||
webView: null,
|
webView: null,
|
||||||
aiChat: null
|
aiChat: null,
|
||||||
|
gallery: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||||
|
|||||||
@ -34,6 +34,9 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
|
|||||||
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), icon: "bx-pen" },
|
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), icon: "bx-pen" },
|
||||||
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), icon: "bx-selection" },
|
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), icon: "bx-selection" },
|
||||||
|
|
||||||
|
// Media notes
|
||||||
|
{ type: "gallery", mime: "", title: t("note_types.gallery"), icon: "bx-image-alt", reserved: false },
|
||||||
|
|
||||||
// Map notes
|
// Map notes
|
||||||
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), icon: "bx-sitemap" },
|
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), icon: "bx-sitemap" },
|
||||||
{ type: "noteMap", mime: "", title: t("note_types.note-map"), icon: "bxs-network-chart", static: true },
|
{ type: "noteMap", mime: "", title: t("note_types.note-map"), icon: "bxs-network-chart", static: true },
|
||||||
|
|||||||
@ -1673,7 +1673,8 @@
|
|||||||
"ai-chat": "AI Chat",
|
"ai-chat": "AI Chat",
|
||||||
"task-list": "Task List",
|
"task-list": "Task List",
|
||||||
"new-feature": "New",
|
"new-feature": "New",
|
||||||
"collections": "Collections"
|
"collections": "Collections",
|
||||||
|
"gallery": "Gallery"
|
||||||
},
|
},
|
||||||
"protect_note": {
|
"protect_note": {
|
||||||
"toggle-on": "Protect the note",
|
"toggle-on": "Protect the note",
|
||||||
@ -2107,5 +2108,43 @@
|
|||||||
"clear-color": "Clear note color",
|
"clear-color": "Clear note color",
|
||||||
"set-color": "Set note color",
|
"set-color": "Set note color",
|
||||||
"set-custom-color": "Set custom note color"
|
"set-custom-color": "Set custom note color"
|
||||||
|
},
|
||||||
|
"gallery": {
|
||||||
|
"no_images": "No Images",
|
||||||
|
"upload_first_image": "Upload First Image",
|
||||||
|
"image_count": "{{count}} Image",
|
||||||
|
"image_count_other": "{{count}} Images",
|
||||||
|
"upload_images": "Upload Images",
|
||||||
|
"confirm_delete_note": "Are you sure you want to delete {{title}}",
|
||||||
|
"confirm_delete_attachment": "Are you sure you want to delete the attachment {{title}}",
|
||||||
|
"delete_note_success": "{{title}} deleted successfully",
|
||||||
|
"delete_attachment_success": "{{title}} deleted successfully",
|
||||||
|
"delete_error": "An error occurred while attempting to delete {{title}} - {{message}}",
|
||||||
|
"confirm_delete_multiple": "Are you sure you want to delete {{count}} items?",
|
||||||
|
"delete_multiple_success": "{{count}} items deleted successfully!",
|
||||||
|
"delete_multiple_error": "An error has occurred while attempting to delete {{count}} images.",
|
||||||
|
"cancel_selection": "Cancel Multi Selection",
|
||||||
|
"select": "Select Multiple",
|
||||||
|
"select_all": "Select All",
|
||||||
|
"deselect_all": "Deselect All",
|
||||||
|
"delete_selected": "Delete {{count}} Item",
|
||||||
|
"delete_selected_other": "Delete {{count}} Items",
|
||||||
|
"delete": "Delete",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"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",
|
||||||
|
"copy_image_link": "Image Link Copied to Clipboard",
|
||||||
|
"share_copy_failed": "Failed to copy image link to clipboard.",
|
||||||
|
"select_image": "Select Image",
|
||||||
|
"show_child_images_btn": "Show Child Images",
|
||||||
|
"hide_child_images_btn": "Hide Child Images",
|
||||||
|
"toggle_child_images_tooltip": "Toggle Child Images",
|
||||||
|
"not_shared": "The gallery is not shared",
|
||||||
|
"shared_success": "The gallery is now being shared",
|
||||||
|
"unshared_success": "The gallery has stopped being shared",
|
||||||
|
"image_not_found": "Image not found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,5 +139,10 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
|
|||||||
view: () => import("./type_widgets/AiChat"),
|
view: () => import("./type_widgets/AiChat"),
|
||||||
className: "ai-chat-widget-container",
|
className: "ai-chat-widget-container",
|
||||||
isFullHeight: true
|
isFullHeight: true
|
||||||
|
},
|
||||||
|
gallery: {
|
||||||
|
view: () => import("./type_widgets/Gallery"),
|
||||||
|
className: "note-detail-gallery",
|
||||||
|
isFullHeight: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
410
apps/client/src/widgets/type_widgets/Gallery.css
Normal file
410
apps/client/src/widgets/type_widgets/Gallery.css
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
.gallery-container {
|
||||||
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--main-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-count {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Masonry Grid Layout */
|
||||||
|
.gallery-masonry {
|
||||||
|
column-count: 4;
|
||||||
|
column-gap: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.gallery-masonry {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.gallery-masonry {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.gallery-masonry {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--main-background-color);
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
padding: 2rem 1rem 0.75rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover .gallery-item-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-type {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.gallery-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-empty-icon {
|
||||||
|
font-size: 5rem;
|
||||||
|
opacity: 0.3;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-empty p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-empty-hint {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lightbox */
|
||||||
|
.gallery-lightbox {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-content {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(90vh - 60px);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-counter {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
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-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
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-nav:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-prev {
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-next {
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection Mode */
|
||||||
|
.gallery-item.selection-mode {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item.selected {
|
||||||
|
border: 3px solid var(--button-border-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
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;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-checkbox input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--button-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
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;
|
||||||
|
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-delete:hover {
|
||||||
|
background: rgba(220, 53, 69, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-lightbox-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 5rem;
|
||||||
|
background: rgba(220, 53, 69, 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-delete:hover {
|
||||||
|
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;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-selection-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
color: var(--danger-color, #dc3545);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: rgba(220, 53, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-link {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
background: rgba(108, 117, 125, 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, opacity 0.2s ease;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover .gallery-item-link {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item-link:hover {
|
||||||
|
background: rgba(108, 117, 125, 1);
|
||||||
|
}
|
||||||
569
apps/client/src/widgets/type_widgets/Gallery.tsx
Normal file
569
apps/client/src/widgets/type_widgets/Gallery.tsx
Normal file
@ -0,0 +1,569 @@
|
|||||||
|
import { TypeWidgetProps } from "./type_widget";
|
||||||
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
|
import { useTriliumEvent, useTriliumOption } from "../react/hooks";
|
||||||
|
import { ParentComponent } from "../react/react_utils";
|
||||||
|
import Button from "../react/Button";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import dialog from "../../services/dialog";
|
||||||
|
import server from "../../services/server";
|
||||||
|
import toast from "../../services/toast";
|
||||||
|
import froca from "../../services/froca";
|
||||||
|
import appContext from "../../components/app_context";
|
||||||
|
import { formatSize } from "../../services/utils";
|
||||||
|
import branches from "../../services/branches";
|
||||||
|
import sync from "../../services/sync";
|
||||||
|
import "./Gallery.css";
|
||||||
|
|
||||||
|
interface ImageItem {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
type: 'note' | 'attachment';
|
||||||
|
noteId?: string; // For attachments, store the parent note ID
|
||||||
|
size?: number; // Size in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Gallery({ note }: TypeWidgetProps) {
|
||||||
|
const [images, setImages] = useState<ImageItem[]>([]);
|
||||||
|
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
|
||||||
|
const [selectedImages, setSelectedImages] = useState<Set<string>>(new Set());
|
||||||
|
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||||
|
const [totalSize, setTotalSize] = useState<number>(0);
|
||||||
|
const [isGalleryShared, setIsGalleryShared] = useState<boolean>(false);
|
||||||
|
const [syncServerHost] = useTriliumOption("syncServerHost");
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
|
||||||
|
async function loadImages() {
|
||||||
|
const imageItems: ImageItem[] = [];
|
||||||
|
|
||||||
|
// Check if the gallery note itself is shared
|
||||||
|
const galleryIsShared = note.hasAncestor("_share");
|
||||||
|
setIsGalleryShared(galleryIsShared);
|
||||||
|
|
||||||
|
// Check if we should hide child images
|
||||||
|
const hideChildImages = note.hasLabel("hideChildAttachments");
|
||||||
|
|
||||||
|
// First, check for images attached directly to this gallery note
|
||||||
|
const directAttachments = await note.getAttachments();
|
||||||
|
const directImageAttachments = directAttachments.filter(a => a.role === "image");
|
||||||
|
|
||||||
|
// Process direct attachments
|
||||||
|
for (const attachment of directImageAttachments) {
|
||||||
|
imageItems.push({
|
||||||
|
id: attachment.attachmentId,
|
||||||
|
url: `api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}`,
|
||||||
|
title: attachment.title,
|
||||||
|
type: 'attachment' as const,
|
||||||
|
noteId: attachment.ownerId,
|
||||||
|
size: attachment.contentLength || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only load child notes and their attachments if hideChildImages is not set
|
||||||
|
if (!hideChildImages) {
|
||||||
|
// Then check child notes
|
||||||
|
const childNotes = await note.getChildNotes();
|
||||||
|
const imageNotes = childNotes.filter(n => n.type === "image");
|
||||||
|
|
||||||
|
// Process image notes in parallel
|
||||||
|
const imageBlobPromises = imageNotes.map(async (imageNote) => {
|
||||||
|
const blob = await imageNote.getBlob();
|
||||||
|
const size = blob?.contentLength || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: imageNote.noteId,
|
||||||
|
url: `api/images/${imageNote.noteId}/${encodeURIComponent(imageNote.title)}`,
|
||||||
|
title: imageNote.title,
|
||||||
|
type: 'note' as const,
|
||||||
|
size
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageNoteItems = await Promise.all(imageBlobPromises);
|
||||||
|
imageItems.push(...imageNoteItems);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
return imageAttachments.map(attachment => ({
|
||||||
|
id: attachment.attachmentId,
|
||||||
|
url: `api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}`,
|
||||||
|
title: attachment.title,
|
||||||
|
type: 'attachment' as const,
|
||||||
|
noteId: attachment.ownerId,
|
||||||
|
size: attachment.contentLength || 0
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachmentArrays = await Promise.all(attachmentPromises);
|
||||||
|
const allAttachments = attachmentArrays.flat();
|
||||||
|
|
||||||
|
imageItems.push(...allAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total size once at the end from all items
|
||||||
|
const calculatedTotalSize = imageItems.reduce((sum, item) => sum + (item.size || 0), 0);
|
||||||
|
|
||||||
|
setImages(imageItems);
|
||||||
|
setTotalSize(calculatedTotalSize);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
loadImages();
|
||||||
|
}, [note]);
|
||||||
|
|
||||||
|
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||||
|
const childNoteIds = images.map(img => img.noteId).filter(Boolean);
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
if (shouldReload) {
|
||||||
|
loadImages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleUploadImages() {
|
||||||
|
parentComponent?.triggerCommand("showUploadAttachmentsDialog", { noteId: note.noteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageClick(img: ImageItem, index: number, e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isSelectionMode) {
|
||||||
|
toggleImageSelection(img.id);
|
||||||
|
} else {
|
||||||
|
setSelectedImage(img);
|
||||||
|
setSelectedIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLinkClick(img: ImageItem, e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (img.type === 'note') {
|
||||||
|
// Navigate to the note
|
||||||
|
appContext.tabManager.getActiveContext()?.setNote(img.id);
|
||||||
|
} else if (img.noteId && img.noteId !== note.noteId) {
|
||||||
|
// For attachments on child notes, navigate to the parent note
|
||||||
|
appContext.tabManager.getActiveContext()?.setNote(img.noteId);
|
||||||
|
}
|
||||||
|
// Note: No else clause - we don't do anything for gallery note attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopyImageLink(img: ImageItem, e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!isGalleryShared) {
|
||||||
|
toast.showError(t("gallery.not_shared"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageUrl: string;
|
||||||
|
|
||||||
|
if (img.type === 'note') {
|
||||||
|
const imageNote = froca.getNoteFromCache(img.id);
|
||||||
|
if (!imageNote) {
|
||||||
|
toast.showError(t("gallery.image_not_found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareId = imageNote.getOwnedLabelValue("shareAlias") || img.id;
|
||||||
|
imageUrl = getAbsoluteUrl(`/share/api/images/${shareId}/${encodeURIComponent(img.title)}`);
|
||||||
|
} else {
|
||||||
|
imageUrl = getAbsoluteUrl(`/share/api/attachments/${img.id}/image/${encodeURIComponent(img.title)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(imageUrl);
|
||||||
|
toast.showMessage(t("gallery.copy_image_link"));
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(t("gallery.share_copy_failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleHideChildImages() {
|
||||||
|
const currentValue = note.hasLabel("hideChildAttachments");
|
||||||
|
|
||||||
|
if (currentValue) {
|
||||||
|
// Remove the attribute
|
||||||
|
const attr = note.getOwnedAttributes("label", "hideChildAttachments")[0];
|
||||||
|
if (attr) {
|
||||||
|
await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
|
||||||
|
toast.showMessage(t("gallery.show_child_images_btn"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add the attribute
|
||||||
|
await server.post(`notes/${note.noteId}/attributes`, {
|
||||||
|
type: "label",
|
||||||
|
name: "hideChildAttachments",
|
||||||
|
value: ""
|
||||||
|
});
|
||||||
|
toast.showMessage(t("gallery.hide_child_images_btn"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbsoluteUrl(path: string): string {
|
||||||
|
if (syncServerHost) {
|
||||||
|
return new URL(path, syncServerHost).href;
|
||||||
|
}
|
||||||
|
const origin = `${location.protocol}//${location.host}`;
|
||||||
|
const pathname = location.pathname.replace(/\/$/, '');
|
||||||
|
return `${origin}${pathname}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleImageSelection(imageId: string) {
|
||||||
|
setSelectedImages(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(imageId)) {
|
||||||
|
newSet.delete(imageId);
|
||||||
|
} else {
|
||||||
|
newSet.add(imageId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectionMode() {
|
||||||
|
setIsSelectionMode(!isSelectionMode);
|
||||||
|
if (isSelectionMode) {
|
||||||
|
setSelectedImages(new Set());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAll() {
|
||||||
|
setSelectedImages(new Set(images.map(img => img.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectAll() {
|
||||||
|
setSelectedImages(new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox() {
|
||||||
|
setSelectedImage(null);
|
||||||
|
setSelectedIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateImage(direction: 'prev' | 'next') {
|
||||||
|
if (images.length === 0) return;
|
||||||
|
|
||||||
|
let newIndex = selectedIndex;
|
||||||
|
if (direction === 'prev') {
|
||||||
|
newIndex = selectedIndex > 0 ? selectedIndex - 1 : images.length - 1;
|
||||||
|
} else {
|
||||||
|
newIndex = selectedIndex < images.length - 1 ? selectedIndex + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedIndex(newIndex);
|
||||||
|
setSelectedImage(images[newIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteImage(img: ImageItem, e?: MouseEvent) {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this image belongs to another note
|
||||||
|
const belongsToOtherNote = img.type === 'note' || (img.noteId && img.noteId !== note.noteId);
|
||||||
|
|
||||||
|
let confirmMessage = img.type === 'note'
|
||||||
|
? t("gallery.confirm_delete_note", { title: img.title })
|
||||||
|
: t("gallery.confirm_delete_attachment", { title: img.title });
|
||||||
|
|
||||||
|
// Add warning if it belongs to another note
|
||||||
|
if (belongsToOtherNote) {
|
||||||
|
confirmMessage += "\n\n" + t("gallery.warning_delete_other_note");
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await dialog.confirm(confirmMessage);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (img.type === 'note') {
|
||||||
|
await server.remove(`notes/${img.id}`);
|
||||||
|
toast.showMessage(t("gallery.delete_note_success", { title: img.title }));
|
||||||
|
} else {
|
||||||
|
await server.remove(`attachments/${img.id}`);
|
||||||
|
toast.showMessage(t("gallery.delete_attachment_success", { title: img.title }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedImage?.id === img.id) {
|
||||||
|
closeLightbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadImages();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.showError(t("gallery.delete_error", { message: error.message || String(error), title: img.title }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteSelected() {
|
||||||
|
if (selectedImages.size === 0) return;
|
||||||
|
|
||||||
|
const confirmMessage = t("gallery.confirm_delete_multiple", { count: selectedImages.size });
|
||||||
|
|
||||||
|
if (!await dialog.confirm(confirmMessage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
successCount++;
|
||||||
|
} 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 }));
|
||||||
|
}
|
||||||
|
if (errorCount > 0) {
|
||||||
|
toast.showError(t("gallery.delete_multiple_error", { count: errorCount }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedImages(new Set());
|
||||||
|
setIsSelectionMode(false);
|
||||||
|
await loadImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!selectedImage) return;
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeLightbox();
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
navigateImage('prev');
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
navigateImage('next');
|
||||||
|
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
handleDeleteImage(selectedImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedImage) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
|
}, [selectedImage, selectedIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gallery-container">
|
||||||
|
<div className="gallery-toolbar">
|
||||||
|
<div className="gallery-toolbar-left">
|
||||||
|
<Button
|
||||||
|
icon="bx bx-upload"
|
||||||
|
text={t("gallery.upload_images")}
|
||||||
|
onClick={handleUploadImages}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={isSelectionMode ? "bx bx-x" : "bx bx-select-multiple"}
|
||||||
|
text={isSelectionMode ? t("gallery.cancel_selection") : t("gallery.select")}
|
||||||
|
onClick={toggleSelectionMode}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={note.hasLabel("hideChildAttachments") ? "bx bx-show" : "bx bx-hide"}
|
||||||
|
text={note.hasLabel("hideChildAttachments") ? t("gallery.show_child_images_btn") : t("gallery.hide_child_images_btn")}
|
||||||
|
onClick={handleToggleHideChildImages}
|
||||||
|
size="small"
|
||||||
|
title={t("gallery.toggle_child_images_tooltip")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSelectionMode && (
|
||||||
|
<div className="gallery-selection-actions">
|
||||||
|
<Button
|
||||||
|
text={t("gallery.select_all")}
|
||||||
|
onClick={selectAll}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
text={t("gallery.deselect_all")}
|
||||||
|
onClick={deselectAll}
|
||||||
|
size="small"
|
||||||
|
disabled={selectedImages.size === 0}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="bx bx-trash"
|
||||||
|
text={t("gallery.delete_selected", { count: selectedImages.size })}
|
||||||
|
onClick={handleDeleteSelected}
|
||||||
|
size="small"
|
||||||
|
disabled={selectedImages.size === 0}
|
||||||
|
className="btn-danger"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="gallery-count">
|
||||||
|
{t("gallery.image_count", { count: images.length })}
|
||||||
|
{totalSize > 0 && ` (${formatSize(totalSize)})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{images.length === 0 ? (
|
||||||
|
<div className="gallery-empty">
|
||||||
|
<div className="gallery-empty-icon">
|
||||||
|
<span className="bx bx-image-alt"></span>
|
||||||
|
</div>
|
||||||
|
<p>{t("gallery.no_images")}</p>
|
||||||
|
<Button
|
||||||
|
icon="bx bx-upload"
|
||||||
|
text={t("gallery.upload_first_image")}
|
||||||
|
onClick={handleUploadImages}
|
||||||
|
primary
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="gallery-masonry">
|
||||||
|
{images.map((img, index) => (
|
||||||
|
<div
|
||||||
|
className={`gallery-item ${selectedImages.has(img.id) ? 'selected' : ''} ${isSelectionMode ? 'selection-mode' : ''}`}
|
||||||
|
key={img.id}
|
||||||
|
onClick={(e) => handleImageClick(img, index, e)}
|
||||||
|
>
|
||||||
|
{isSelectionMode && (
|
||||||
|
<div className="gallery-item-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedImages.has(img.id)}
|
||||||
|
onChange={() => toggleImageSelection(img.id)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
aria-label={t("gallery.select_image", { title: img.title })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<img src={img.url} alt={img.title} loading="lazy" />
|
||||||
|
<div className="gallery-item-overlay">
|
||||||
|
<div className="gallery-item-title">{img.title}</div>
|
||||||
|
{!isSelectionMode && (
|
||||||
|
<div className="gallery-item-actions">
|
||||||
|
{isGalleryShared && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-item-copy-link"
|
||||||
|
onClick={(e) => handleCopyImageLink(img, e)}
|
||||||
|
title={t("gallery.copy_image_link")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-link"></span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-item-delete"
|
||||||
|
onClick={(e) => handleDeleteImage(img, e)}
|
||||||
|
title={t("gallery.delete")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-trash"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isSelectionMode && (img.type === 'note' || (img.noteId && img.noteId !== note.noteId)) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-item-link"
|
||||||
|
onClick={(e) => handleLinkClick(img, e)}
|
||||||
|
title={t("gallery.goto_note")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-right-arrow-alt"></span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedImage && !isSelectionMode && (
|
||||||
|
<div className="gallery-lightbox" onClick={closeLightbox}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-lightbox-close"
|
||||||
|
onClick={closeLightbox}
|
||||||
|
title={t("gallery.close")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-x"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-lightbox-delete"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleDeleteImage(selectedImage); }}
|
||||||
|
title={t("gallery.delete")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-trash"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isGalleryShared && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-lightbox-copy-link"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleCopyImageLink(selectedImage, e); }}
|
||||||
|
title={t("gallery.copy_image_link")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-link"></span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-lightbox-nav gallery-lightbox-prev"
|
||||||
|
onClick={(e) => { e.stopPropagation(); navigateImage('prev'); }}
|
||||||
|
title={t("gallery.previous")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-chevron-left"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="gallery-lightbox-nav gallery-lightbox-next"
|
||||||
|
onClick={(e) => { e.stopPropagation(); navigateImage('next'); }}
|
||||||
|
title={t("gallery.next")}
|
||||||
|
>
|
||||||
|
<span className="bx bx-chevron-right"></span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="gallery-lightbox-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<img src={selectedImage.url} alt={selectedImage.title} />
|
||||||
|
<div className="gallery-lightbox-info">
|
||||||
|
<h3>{selectedImage.title}</h3>
|
||||||
|
<span className="gallery-lightbox-counter">
|
||||||
|
{selectedIndex + 1} / {images.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4414,6 +4414,7 @@ components:
|
|||||||
- contentWidget
|
- contentWidget
|
||||||
- mindMap
|
- mindMap
|
||||||
- geoMap
|
- geoMap
|
||||||
|
- gallery
|
||||||
|
|
||||||
Branch:
|
Branch:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@ -15,7 +15,8 @@ const noteTypes = [
|
|||||||
{ type: "doc", defaultMime: "" },
|
{ type: "doc", defaultMime: "" },
|
||||||
{ type: "contentWidget", defaultMime: "" },
|
{ type: "contentWidget", defaultMime: "" },
|
||||||
{ type: "mindMap", defaultMime: "application/json" },
|
{ type: "mindMap", defaultMime: "application/json" },
|
||||||
{ type: "aiChat", defaultMime: "application/json" }
|
{ type: "aiChat", defaultMime: "application/json" },
|
||||||
|
{ type: "gallery", defaultMime: "" }
|
||||||
];
|
];
|
||||||
|
|
||||||
function getDefaultMimeForNoteType(typeName: string) {
|
function getDefaultMimeForNoteType(typeName: string) {
|
||||||
|
|||||||
@ -120,7 +120,8 @@ export const ALLOWED_NOTE_TYPES = [
|
|||||||
"webView",
|
"webView",
|
||||||
"code",
|
"code",
|
||||||
"mindMap",
|
"mindMap",
|
||||||
"aiChat"
|
"aiChat",
|
||||||
|
"gallery"
|
||||||
] as const;
|
] as const;
|
||||||
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
|
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user