added imageViewer
authorPhiTux <redacted>
Thu, 3 Apr 2025 16:03:18 +0000 (18:03 +0200)
committerPhiTux <redacted>
Thu, 3 Apr 2025 16:03:18 +0000 (18:03 +0200)
frontend/src/lib/ImageViewer.svelte [new file with mode: 0644]
frontend/src/routes/read/+page.svelte
frontend/src/routes/write/+page.svelte

diff --git a/frontend/src/lib/ImageViewer.svelte b/frontend/src/lib/ImageViewer.svelte
new file mode 100644 (file)
index 0000000..2ea5106
--- /dev/null
@@ -0,0 +1,324 @@
+<script>
+       import { onMount } from 'svelte';
+       import { fade, slide } from 'svelte/transition';
+       import { faXmark, faChevronRight, faChevronLeft } from '@fortawesome/free-solid-svg-icons';
+       import { Fa } from 'svelte-fa';
+
+       let { images } = $props(); // Array of image objects with `src`, `filename`, and `uuid_filename`
+
+       let fullscreen = $state(false);
+       let currentIndex = $state(0);
+
+       function openFullscreen(index) {
+               fullscreen = true;
+               currentIndex = index;
+       }
+
+       function closeFullscreen() {
+               fullscreen = false;
+       }
+
+       function nextImage() {
+               if (currentIndex < images.length - 1) {
+                       currentIndex++;
+               }
+       }
+
+       function prevImage() {
+               if (currentIndex > 0) {
+                       currentIndex--;
+               }
+       }
+
+       function handleKeyDown(event) {
+               if (!fullscreen) return;
+
+               if (event.key === 'ArrowRight') {
+                       nextImage();
+               } else if (event.key === 'ArrowLeft') {
+                       prevImage();
+               } else if (event.key === 'Escape') {
+                       closeFullscreen();
+               }
+       }
+
+       // Variablen für die Swipe-Erkennung
+       let touchStartX = 0;
+       let touchEndX = 0;
+
+       // Swipe-Handler
+       function handleTouchStart(event) {
+               touchStartX = event.touches[0].clientX; // X-Position des Touch-Starts speichern
+       }
+
+       function handleTouchMove(event) {
+               touchEndX = event.touches[0].clientX; // X-Position während der Bewegung speichern
+       }
+
+       function handleTouchEnd() {
+               // Prüfen, ob der Swipe nach links oder rechts ging
+               if (touchStartX - touchEndX > 50) {
+                       // Swipe nach links
+                       nextImage();
+               } else if (touchEndX - touchStartX > 50) {
+                       // Swipe nach rechts
+                       prevImage();
+               }
+       }
+
+       onMount(() => {
+               window.addEventListener('keydown', handleKeyDown);
+               return () => {
+                       window.removeEventListener('keydown', handleKeyDown);
+               };
+       });
+
+       let fullscreenContainer = $state();
+       $effect(() => {
+               if (fullscreen && fullscreenContainer) {
+                       document.body.appendChild(fullscreenContainer);
+               } else {
+                       fullscreenContainer?.remove();
+               }
+       });
+</script>
+
+<!-- Fullscreen Image Viewer -->
+{#if fullscreen}
+       <!-- svelte-ignore a11y_click_events_have_key_events -->
+       <!-- svelte-ignore a11y_no_static_element_interactions -->
+       <div
+               bind:this={fullscreenContainer}
+               transition:fade={{ duration: 100 }}
+               class="fullscreen-overlay"
+               onclick={(event) => {
+                       if (
+                               event.target.classList.contains('fullscreen-image-container') ||
+                               event.target.classList.contains('fullscreen-overlay')
+                       ) {
+                               closeFullscreen();
+                       }
+               }}
+               ontouchstart={handleTouchStart}
+               ontouchmove={handleTouchMove}
+               ontouchend={handleTouchEnd}
+       >
+               <button class="close-btn" onclick={closeFullscreen}><Fa icon={faXmark} fw /> </button>
+               <div class="fullscreen-image-container">
+                       <button class="nav-btn prev" onclick={prevImage} disabled={currentIndex === 0}>
+                               <Fa icon={faChevronLeft} fw />
+                       </button>
+                       {#key images[currentIndex].uuid_filename}
+                               <img
+                                       class="fullscreen-image"
+                                       alt={images[currentIndex].filename}
+                                       src={images[currentIndex].src}
+                               />
+                       {/key}
+                       <div class="image-info">
+                               <span class="image-name">{images[currentIndex].filename}</span>
+                               <a
+                                       class="download-btn"
+                                       href={images[currentIndex].src}
+                                       download={images[currentIndex].filename}
+                               >
+                                       Download
+                               </a>
+                       </div>
+                       <button
+                               class="nav-btn next"
+                               onclick={nextImage}
+                               disabled={currentIndex === images.length - 1}
+                       >
+                               <Fa icon={faChevronRight} fw />
+                       </button>
+               </div>
+               <div class="horizontal-scroll fullscreen-scroll">
+                       {#each images as image, index (image.uuid_filename)}
+                               <button
+                                       type="button"
+                                       class="image-container {index === currentIndex ? 'active' : ''}"
+                                       onclick={() => (currentIndex = index)}
+                               >
+                                       <img class="image" alt={image.filename} src={image.src} />
+                               </button>
+                       {/each}
+               </div>
+       </div>
+{/if}
+
+<div class="image-gallery">
+       <!-- Horizontal Scrollbar -->
+       <div class="horizontal-scroll px-2">
+               {#each images as image, index (image.uuid_filename)}
+                       <button
+                               type="button"
+                               class="image-container"
+                               onclick={() => openFullscreen(index)}
+                               transition:slide={{ axis: 'x' }}
+                       >
+                               <img class="image" alt={image.filename} src={image.src} transition:fade />
+                       </button>
+               {/each}
+       </div>
+</div>
+
+<style>
+       .image-info {
+               position: absolute;
+               bottom: 0;
+               right: 0;
+               background: rgba(0, 0, 0, 0); /* Semi-transparent background */
+               color: white;
+               display: flex;
+               justify-content: right;
+               align-items: center;
+               padding: 0.5rem 1rem;
+               box-sizing: border-box;
+               transition: background 0.2s ease;
+       }
+
+       .image-info:hover {
+               background: rgba(0, 0, 0, 0.6);
+       }
+
+       .image-name {
+               font-size: 1rem;
+               overflow: hidden;
+               text-overflow: ellipsis;
+               white-space: nowrap;
+               padding-right: 1rem;
+       }
+
+       .download-btn {
+               color: white;
+               text-decoration: none;
+               font-size: 1rem;
+               background: rgba(255, 255, 255, 0.2);
+               padding: 0.3rem 0.6rem;
+               border-radius: 4px;
+               transition: background 0.3s ease;
+       }
+
+       .download-btn:hover {
+               background: rgba(255, 255, 255, 0.4);
+       }
+
+       .image-gallery {
+               display: flex;
+               flex-direction: column;
+               gap: 1rem;
+       }
+
+       .horizontal-scroll {
+               display: flex;
+               gap: 1rem;
+               overflow-x: auto;
+               padding: 0.5rem 0;
+       }
+
+       .image-container {
+               border: none;
+               background: transparent;
+               padding: 0;
+               cursor: pointer;
+               transition: transform 0.2s ease;
+       }
+
+       .image-container:hover .image {
+               transform: scale(1.1);
+               box-shadow: 0 0 12px 3px rgba(0, 0, 0, 0.2);
+       }
+
+       .image {
+               max-width: 150px;
+               max-height: 100px;
+               border-radius: 8px;
+               transition: transform 0.3s ease;
+       }
+
+       .fullscreen-overlay {
+               position: fixed;
+               top: 0;
+               left: 0;
+               width: 100vw;
+               height: 100vh;
+               background: rgba(0, 0, 0, 0.9);
+               display: flex;
+               flex-direction: column;
+               justify-content: space-between;
+               align-items: center;
+               z-index: 9999;
+               color: white;
+       }
+
+       .fullscreen-image-container {
+               display: flex;
+               align-items: center;
+               justify-content: center;
+               position: relative;
+               width: 100%;
+               height: 80%;
+               flex: 1 0;
+       }
+
+       .fullscreen-image {
+               max-width: 100%;
+               max-height: 95%;
+               border-radius: 8px;
+       }
+
+       .nav-btn {
+               position: absolute;
+               top: 50%;
+               transform: translateY(-50%);
+               background: rgba(0, 0, 0, 0.5);
+               border: none;
+               color: white;
+               font-size: 1.5rem;
+               padding: 0.5rem 1rem;
+               cursor: pointer;
+               z-index: 10000;
+       }
+
+       .nav-btn.prev {
+               left: 1rem;
+       }
+
+       .nav-btn.next {
+               right: 1rem;
+       }
+
+       .nav-btn:disabled {
+               opacity: 0.5;
+               cursor: not-allowed;
+       }
+
+       .close-btn {
+               display: flex;
+               align-items: center;
+               padding: 10px;
+
+               position: absolute;
+               top: 1rem;
+               right: 1rem;
+               background: none;
+               border: none;
+               color: white;
+               font-size: 2rem;
+               cursor: pointer;
+               z-index: 10000;
+       }
+
+       .fullscreen-scroll {
+               width: 100%;
+               display: flex;
+               justify-content: center;
+               padding: 0.5rem;
+               background: rgba(0, 0, 0, 0.8);
+       }
+
+       .fullscreen-scroll .image-container.active .image {
+               outline: 2px solid white;
+       }
+</style>
index f599d69fc1e65d67e5ca98e7dc33f1772264a9b5..a37e7c8cce54e7ea7384b17cc728f1fd2350444c 100644 (file)
@@ -13,6 +13,7 @@
        import { faCloudArrowDown } from '@fortawesome/free-solid-svg-icons';
        import { Fa } from 'svelte-fa';
        import { fade, slide } from 'svelte/transition';
+       import ImageViewer from '$lib/ImageViewer.svelte';
 
        marked.use({
                breaks: true,
@@ -63,6 +64,9 @@
 
        $effect(() => {
                if ($selectedDate) {
+                       $cal.currentYear = $selectedDate.getFullYear();
+                       $cal.currentMonth = $selectedDate.getMonth();
+
                        let el = document.querySelector(`.log[data-log-day="${$selectedDate.getDate()}"]`);
                        if (el) {
                                el.scrollIntoView({ behavior: 'smooth', block: 'start' });
                                                </b>
                                        </p>
                                </div>
-                               <div class="flex-grow-1">
+                               <div class="flex-grow-1 middle">
                                        {#if log.text && log.text !== ''}
                                                <div class="text">
                                                        {@html marked.parse(log.text)}
                                                                </button>
                                                        </div>
                                                {:else}
-                                                       <div class="d-flex flex-row images mt-3">
-                                                               {#each log.images as image (image.uuid_filename)}
-                                                                       <button
-                                                                               type="button"
-                                                                               onclick={() => {
-                                                                                       viewImage(image.uuid_filename);
-                                                                               }}
-                                                                               class="imageContainer d-flex align-items-center position-relative"
-                                                                               transition:slide={{ axis: 'x' }}
-                                                                       >
-                                                                               {#if image.src}
-                                                                                       <img transition:fade class="image" alt={image.filename} src={image.src} />
-                                                                               {:else}
-                                                                                       <div class="spinner-border" role="status">
-                                                                                               <span class="visually-hidden">Loading...</span>
-                                                                                       </div>
-                                                                               {/if}
-                                                                       </button>
-                                                               {/each}
-                                                       </div>
+                                                       <ImageViewer images={log.images} />
                                                {/if}
                                        {/if}
                                </div>
 
                                {#if log.files && log.files.length > 0}
-                                       <div class="d-flex flex-column">
+                                       <div class="d-flex flex-column ms-3 files">
                                                <FileList files={log.files} {downloadFile} />
                                        </div>
                                {/if}
 </div>
 
 <style>
+       .files {
+               max-width: 350px;
+       }
+
+       .middle {
+               overflow-x: auto;
+       }
+
        .loadImageBtn {
                padding: 0.5rem 1rem;
                border: none;
                background-color: #bbb;
        }
 
-       .images {
-               gap: 1rem;
-               overflow-x: auto;
-       }
-
-       .image,
-       .imageContainer {
-               border-radius: 8px;
-       }
-
-       .imageContainer {
-               min-height: 80px;
-               padding: 0px;
-               border: 0px;
-               background-color: transparent;
-               overflow: clip;
-       }
-
-       .image:hover {
-               transform: scale(1.1);
-               box-shadow: 0 0 12px 3px rgba(0, 0, 0, 0.2);
-       }
-
-       .image {
-               max-width: 250px;
-               max-height: 150px;
-               transition: all ease 0.3s;
-       }
-
        .text {
                word-wrap: anywhere;
        }
index 61ec8a4422dce103f73859f3199775434fbe5924..59f8e17d5b7cd3a52f283e263eb6483425c82973 100644 (file)
@@ -26,6 +26,7 @@
        import TagModal from '$lib/TagModal.svelte';
        import FileList from '$lib/FileList.svelte';
        import { formatBytes } from '$lib/helpers.js';
+       import ImageViewer from '$lib/ImageViewer.svelte';
 
        axios.interceptors.request.use((config) => {
                config.withCredentials = true;
                        });
        }
 
-       function viewImage(uuid) {
-               // set active image
-               document.querySelectorAll('.carousel-item').forEach((item) => {
-                       item.classList.remove('active');
-                       if (
-                               item.id ===
-                               'carousel-item-' + images.findIndex((image) => image.uuid_filename === uuid)
-                       ) {
-                               item.classList.add('active');
-                       }
-               });
-               // set active image-button-indicator
-               document.querySelectorAll('.carousel-button').forEach((button) => {
-                       button.classList.remove('active');
-                       if (
-                               button.id ===
-                               'carousel-button-' + images.findIndex((image) => image.uuid_filename === uuid)
-                       ) {
-                               button.classList.add('active');
-                       }
-               });
-
-               const modal = new bootstrap.Modal(document.getElementById('modalImages'));
-               modal.show();
-       }
-
        let searchTab = $state('');
        let showTagDropdown = $state(false);
 
                                        </button>
                                </div>
                        {:else}
-                               <div class="d-flex flex-row images mt-3">
+                               <ImageViewer {images} />
+                               <!-- <div class="d-flex flex-row images mt-3">
                                        {#each images as image (image.uuid_filename)}
                                                <button
                                                        type="button"
                                                        {/if}
                                                </button>
                                        {/each}
-                               </div>
+                               </div> -->
                        {/if}
                {/if}
                {$selectedDate}<br />
                isSaving={isSavingNewTag}
                {saveNewTag}
        />
-
-       <div
-               class="modal fade"
-               id="modalImages"
-               tabindex="-1"
-               aria-labelledby="modalImagesLabel"
-               aria-hidden="true"
-       >
-               <div class="modal-dialog modal-xl modal-fullscreen-sm-down">
-                       <div class="modal-content">
-                               <div class="modal-header d-none d-sm-flex">
-                                       <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
-                                       ></button>
-                               </div>
-
-                               <div class="modal-body">
-                                       <div id="imageCarousel" class="carousel slide carousel-fade">
-                                               <div class="carousel-indicators">
-                                                       {#each images as image, i (image.uuid_filename)}
-                                                               <button
-                                                                       type="button"
-                                                                       data-bs-target="#imageCarousel"
-                                                                       data-bs-slide-to={i}
-                                                                       aria-label="Slide {i}"
-                                                                       class="carousel-button"
-                                                                       id="carousel-button-{i}"
-                                                               ></button>
-                                                       {/each}
-                                               </div>
-                                               <div class="carousel-inner">
-                                                       {#each images as image, i (image.uuid_filename)}
-                                                               <div id="carousel-item-{i}" class="carousel-item">
-                                                                       <img src={image.src} class="d-block w-100" alt={image.filename} />
-                                                                       <div class="carousel-caption d-none d-md-block">
-                                                                               <span class="imageLabelCarousel">{image.filename}</span>
-                                                                               <button
-                                                                                       class="btn btn-primary"
-                                                                                       onclick={() => downloadFile(image.uuid_filename)}
-                                                                               >
-                                                                                       Download
-                                                                               </button>
-                                                                       </div>
-                                                               </div>
-                                                       {/each}
-                                               </div>
-                                               <button
-                                                       class="carousel-control-prev"
-                                                       type="button"
-                                                       data-bs-target="#imageCarousel"
-                                                       data-bs-slide="prev"
-                                               >
-                                                       <span class="carousel-control-prev-icon" aria-hidden="true"></span>
-                                                       <span class="visually-hidden">Previous</span>
-                                               </button>
-                                               <button
-                                                       class="carousel-control-next"
-                                                       type="button"
-                                                       data-bs-target="#imageCarousel"
-                                                       data-bs-slide="next"
-                                               >
-                                                       <span class="carousel-control-next-icon" aria-hidden="true"></span>
-                                                       <span class="visually-hidden">Next</span>
-                                               </button>
-                                       </div>
-                               </div>
-                               <div class="modal-footer d-block d-sm-none">
-                                       <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-                               </div>
-                       </div>
-               </div>
-       </div>
 </div>
 
 <style>
                transition: all ease 0.2s;
        }
 
-       .carousel-item > img {
-               transition: all ease 0.3s;
-       }
-
-       #middle {
-               min-width: 400px;
-       }
-
-       .imageLabelCarousel {
-               font-size: 20px;
-               transition: background-color ease 0.3s;
-               padding: 5px;
-               border-radius: 5px;
-       }
-
-       .carousel-caption:hover > .imageLabelCarousel {
-               background-color: rgba(0, 0, 0, 0.4);
-       }
-
-       .image,
-       .imageContainer {
-               border-radius: 8px;
-       }
-
-       .imageContainer {
-               min-height: 80px;
-               padding: 0px;
-               border: 0px;
-               background-color: transparent;
-               overflow: clip;
-       }
-
-       .image:hover {
-               transform: scale(1.1);
-               box-shadow: 0 0 12px 3px rgba(0, 0, 0, 0.2);
-       }
-
-       .image {
-               max-width: 250px;
-               max-height: 150px;
-               transition: all ease 0.3s;
-       }
-
-       .images {
-               gap: 1rem;
-               overflow-x: auto;
-       }
-
        :global(.modal.show) {
                background-color: rgba(80, 80, 80, 0.1) !important;
                backdrop-filter: blur(2px) saturate(150%);
git clone https://git.99rst.org/PROJECT