Image Zoom

// Trilium Notes Image Zoom Widget
// check for updates:
// https://github.com/Nriver/image-zoom-widget/releases

// Access translations based on the selected language
const i18n = key => translations.trans[config.lang][key];

class ImagePreviewWidget extends api.NoteContextAwareWidget {
    get position() {
        return 100;
    }
    get parentWidget() {
        return 'center-pane';
    }

    isEnabled() {
        return super.isEnabled();
    }

    doRender() {
        this.$widget = $(`
            <style>
                /* Modal box styles */
                .image-preview-modal {
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background-color: rgba(0, 0, 0, 0.8);
                    display: none;
                    justify-content: center;
                    align-items: center;
                    z-index: 10000;
                    overflow: hidden;
                }

                /* Image styles */
                .image-preview-modal img {
                    transition: transform 0.3s ease;
                    cursor: grab;
                    position: relative;
                }

                /* Cursor style when dragging */
                .image-preview-modal img.grabbing {
                    cursor: grabbing;
                }

                /* Right-click menu styles */
                .context-menu {
                    position: absolute;
                    background-color: var(--menu-background-color);
                    border: 1px solid var(--menu-text-color);
                    color: var(--menu-text-color);
                    padding: 5px;
                    z-index: 10001;
                    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
                }

                .context-menu div {
                    padding: 5px;
                    cursor: pointer;
                }

                .context-menu div:hover {
                    background-color: var(--hover-item-background-color);
                    color: var(--hover-item-text-color);
                }
            </style>
        `);
        return this.$widget;
    }

    async refreshWithNote(note) {
        if (note.type !== 'text') {
            return;
        }

        $(document).ready(function () {
            const container = $("div.note-split:not(.hidden-ext) > div.scrolling-container > div.note-detail");

            // Retrieve zoom scale configuration from the config object
            const { minZoomScale, maxZoomScale, zoomFactor, executeDelay, initialDisplayMode, screenPercentage, imageMultiple, previewTrigger } = config;
            if (minZoomScale === undefined || maxZoomScale === undefined || zoomFactor === undefined) {
                console.error('Error: Zoom scale configuration missing!');
                return;
            }

            // Enable image preview functionality
            function enableImagePreview($images) {
                const modal = document.querySelector('.image-preview-modal') || document.createElement('div');
                modal.classList.add('image-preview-modal');
                const modalImage = modal.querySelector('img') || document.createElement('img');
                modal.appendChild(modalImage);
                if (!document.body.contains(modal)) {
                    document.body.appendChild(modal);
                }

                function closeModal() {
                    modal.style.display = 'none';
                    modalImage.style.transform = 'scale(1)';
                    modalImage.dataset.scale = 1;
                    modalImage.style.left = '0px';
                    modalImage.style.top = '0px';
                }

                modal.addEventListener('click', (event) => {
                    if (event.target !== modalImage) {
                        closeModal();
                    }
                });

                $images.each(function () {
                    const img = this;
                    img.style.cursor = 'pointer';

                    const previewHandler = (e) => {
                        e.stopPropagation();
                        modalImage.src = img.src;
                        modal.style.display = 'flex';

                        function setInitialScale() {
                            const padding = 40;
                            const screenWidth = window.innerWidth - padding;
                            const screenHeight = window.innerHeight - padding;
                            const imgWidth = modalImage.naturalWidth;
                            const imgHeight = modalImage.naturalHeight;

                            const widthScale = screenWidth / imgWidth;
                            const heightScale = screenHeight / imgHeight;
                            const maxScale = Math.min(widthScale, heightScale);

                            let initialScale;
                            if (initialDisplayMode === 'screenPercentage') {
                                const minDisplayWidth = screenWidth * screenPercentage;
                                const minDisplayHeight = screenHeight * screenPercentage;
                                const minWidthScale = minDisplayWidth / imgWidth;
                                const minHeightScale = minDisplayHeight / imgHeight;
                                const minScale = Math.max(minWidthScale, minHeightScale);
                                initialScale = Math.min(minScale, maxScale);
                            } else if (initialDisplayMode === 'imageMultiple') {
                                const multipleScale = imageMultiple;
                                initialScale = Math.min(multipleScale, maxScale);
                            }

                            modalImage.dataset.scale = initialScale;
                            modalImage.style.transform = `scale(${initialScale})`;
                        }

                        if (modalImage.complete) {
                            setInitialScale();
                        } else {
                            modalImage.onload = setInitialScale;
                        }
                    };

                    img.removeEventListener('click', previewHandler);
                    img.removeEventListener('dblclick', previewHandler);
                    img.addEventListener(previewTrigger, previewHandler);
                });

                document.addEventListener('keydown', (event) => {
                    if (event.key === 'Escape') {
                        closeModal();
                    }
                });

                let isDragging = false;
                let startX = 0;
                let startY = 0;
                let initialLeft = 0;
                let initialTop = 0;

                modalImage.addEventListener('mousedown', (e) => {
                    isDragging = true;
                    startX = e.clientX;
                    startY = e.clientY;
                    initialLeft = parseInt(modalImage.style.left || 0);
                    initialTop = parseInt(modalImage.style.top || 0);
                    modalImage.classList.add('grabbing');
                    e.preventDefault();
                });

                document.addEventListener('mousemove', (e) => {
                    if (isDragging) {
                        const deltaX = e.clientX - startX;
                        const deltaY = e.clientY - startY;
                        modalImage.style.left = `${initialLeft + deltaX}px`;
                        modalImage.style.top = `${initialTop + deltaY}px`;
                    }
                });

                document.addEventListener('mouseup', () => {
                    if (isDragging) {
                        isDragging = false;
                        modalImage.classList.remove('grabbing');
                    }
                });

                modalImage.dataset.scale = 1;

                // Bind wheel event only once
                if (!modalImage.dataset.wheelBound) {
                    modal.addEventListener('wheel', (event) => {
                        event.preventDefault();
                        let scale = parseFloat(modalImage.dataset.scale);
                        scale *= event.deltaY < 0 ? zoomFactor : 1 / zoomFactor;
                        scale = Math.min(Math.max(scale, minZoomScale), maxZoomScale);
                        console.log('image scale', scale);
                        modalImage.dataset.scale = scale;
                        modalImage.style.transform = `scale(${scale})`;
                    });
                    modalImage.dataset.wheelBound = 'true'; // Mark the wheel event as bound
                }

                // Add right-click menu for download
                modalImage.addEventListener('contextmenu', (e) => {
                    e.preventDefault();

                    const existingMenu = document.querySelector('.context-menu');
                    if (existingMenu) {
                        document.body.removeChild(existingMenu);
                    }

                    const menu = document.createElement('div');
                    menu.classList.add('context-menu');
                    menu.style.top = `${e.clientY}px`;
                    menu.style.left = `${e.clientX}px`;

                    const downloadOption = document.createElement('div');
                    downloadOption.textContent = i18n('downloadImage');
                    downloadOption.addEventListener('click', () => {
                        const a = document.createElement('a');
                        a.href = modalImage.src;
                        a.download = 'image';
                        a.click();
                        document.body.removeChild(menu);
                    });

                    menu.appendChild(downloadOption);

                    document.body.appendChild(menu);

                    document.addEventListener(
                        'click',
                        () => {
                            if (document.body.contains(menu)) {
                                document.body.removeChild(menu);
                            }
                        },
                        { once: true }
                    );
                });
            }

            function bindImageEvents(container) {
                const images = container.find("img");
                enableImagePreview(images);
            }

            // Monitor DOM changes for newly added images
            const observer = new MutationObserver(() => {
                bindImageEvents(container);
            });

            observer.observe(container[0], {
                childList: true,
                subtree: true,
            });

            // Initial bind for existing images
            setTimeout(() => bindImageEvents(container), executeDelay);
        });
    }
}

module.exports = new ImagePreviewWidget();