--- /dev/null
+<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>
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,
$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;
}
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%);