show files and images in reading-mode
authorPhiTux <redacted>
Thu, 27 Mar 2025 18:30:00 +0000 (19:30 +0100)
committerPhiTux <redacted>
Thu, 27 Mar 2025 18:30:00 +0000 (19:30 +0100)
backend/server/routers/logs.py
frontend/src/lib/FileList.svelte [new file with mode: 0644]
frontend/src/lib/helpers.js [new file with mode: 0644]
frontend/src/routes/read/+page.svelte
frontend/src/routes/write/+page.svelte

index 0c54f6facf6fe5931116c2ecacc4c89d7a3bcef0..2616e0ef16f5f170da13302267ce2b26931dfc28 100644 (file)
@@ -277,10 +277,21 @@ async def loadMonthForReading(month: int, year: int, cookie = Depends(users.isLo
     days = []
     enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
     for dayLog in content["days"]:
+        day = {"day": dayLog["day"]}
         if "text" in dayLog.keys():
-            days.append({"day": dayLog["day"], 
-                         "text": security.decrypt_text(dayLog["text"], enc_key), 
-                         "date_written": security.decrypt_text(dayLog["date_written"], enc_key)})
+            day["text"] = security.decrypt_text(dayLog["text"], enc_key)
+            day["date_written"] = security.decrypt_text(dayLog["date_written"], enc_key)
+        if "tags" in dayLog.keys():
+            day["tags"] = dayLog["tags"]
+        if "files" in dayLog.keys():
+            day["files"] = []
+            for file in dayLog["files"]:
+                file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
+                day["files"].append(file)
+
+        # if one of the keys is in day: 
+        if "text" in day or "files" in day or "tags" in day:
+            days.append(day)
     
     days.sort(key=lambda x: x["day"])
 
diff --git a/frontend/src/lib/FileList.svelte b/frontend/src/lib/FileList.svelte
new file mode 100644 (file)
index 0000000..0b6be36
--- /dev/null
@@ -0,0 +1,90 @@
+<script>
+       import { Fa } from 'svelte-fa';
+       import { faTrash } from '@fortawesome/free-solid-svg-icons';
+       import { slide } from 'svelte/transition';
+       import { formatBytes } from './helpers.js';
+
+       let { files, downloadFile, askDeleteFile, deleteAllowed } = $props();
+</script>
+
+{#each files as file (file.uuid_filename)}
+       <div class="btn-group file mt-2" transition:slide>
+               <button
+                       onclick={() => downloadFile(file.uuid_filename)}
+                       class="p-2 fileBtn d-flex flex-column flex-fill"
+               >
+                       <div class="d-flex flex-row align-items-center">
+                               <div class="filename filenameWeight">{file.filename}</div>
+                               <span class="filesize">({formatBytes(file.size)})</span>
+                       </div>
+                       {#if file.downloadProgress >= 0}
+                               <div
+                                       class="progress"
+                                       role="progressbar"
+                                       aria-label="Download progress"
+                                       aria-valuemin="0"
+                                       aria-valuemax="100"
+                               >
+                                       <div
+                                               class="progress-bar overflow-visible bg-info {file.downloadProgress === 0
+                                                       ? 'progress-bar-striped progress-bar-animated'
+                                                       : ''}"
+                                               style:width={file.downloadProgress + '%'}
+                                               aria-valuenow={file.downloadProgress}
+                                               aria-valuemax="100"
+                                       >
+                                               {#if file.downloadProgress === 0}
+                                                       <span class="text-dark">Wird entschlüsselt...</span>
+                                               {:else}
+                                                       <span class="text-dark">Download: {file.downloadProgress}%</span>
+                                               {/if}
+                                       </div>
+                               </div>
+                       {/if}
+               </button>
+               {#if deleteAllowed}
+                       <button
+                               class="p-2 fileBtn deleteFileBtn"
+                               onclick={() => askDeleteFile(file.uuid_filename, file.filename)}
+                               ><Fa icon={faTrash} fw /></button
+                       >
+               {/if}
+       </div>
+{/each}
+
+<style>
+       .filename {
+               padding-right: 0.5rem;
+               word-break: break-word;
+       }
+
+       .filesize {
+               opacity: 0.7;
+               font-size: 0.8rem;
+               white-space: nowrap;
+       }
+
+       .fileBtn {
+               border: 0;
+               background-color: rgba(0, 0, 0, 0);
+               transition: all ease 0.3s;
+       }
+
+       .fileBtn:hover {
+               background-color: rgba(0, 0, 0, 0.1);
+       }
+
+       .deleteFileBtn {
+               border-left: 1px solid rgba(92, 92, 92, 0.445);
+       }
+
+       .deleteFileBtn:hover {
+               color: rgb(165, 0, 0);
+       }
+
+       .file {
+               background-color: rgba(117, 117, 117, 0.45);
+               border: 0px solid #ececec77;
+               border-radius: 5px;
+       }
+</style>
diff --git a/frontend/src/lib/helpers.js b/frontend/src/lib/helpers.js
new file mode 100644 (file)
index 0000000..731e3b0
--- /dev/null
@@ -0,0 +1,13 @@
+function formatBytes(bytes) {
+       if (!+bytes) return '0 Bytes';
+
+       const k = 1024;
+       //const dm = 2; // decimal places
+       const sizes = ['B', 'KB', 'MB', 'GB'];
+
+       const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+       return `${parseFloat((bytes / Math.pow(k, i)).toFixed(0))} ${sizes[i]}`;
+}
+
+export { formatBytes };
index 0aa4c4892fd1f885237a17404d0f50afad1eff63..f599d69fc1e65d67e5ca98e7dc33f1772264a9b5 100644 (file)
@@ -6,6 +6,13 @@
        import Sidenav from '$lib/Sidenav.svelte';
        import { onMount } from 'svelte';
        import { marked } from 'marked';
+       import Tag from '$lib/Tag.svelte';
+       import { tags } from '$lib/tagStore.js';
+       import FileList from '$lib/FileList.svelte';
+       import { autoLoadImages } from '$lib/settingsStore';
+       import { faCloudArrowDown } from '@fortawesome/free-solid-svg-icons';
+       import { Fa } from 'svelte-fa';
+       import { fade, slide } from 'svelte/transition';
 
        marked.use({
                breaks: true,
@@ -45,6 +52,9 @@
        let currentYear = $cal.currentYear;
        $effect(() => {
                if ($cal.currentMonth !== currentMonth || $cal.currentYear !== currentYear) {
+                       cancelDownload.abort();
+                       cancelDownload = new AbortController();
+
                        loadMonthForReading();
                        currentMonth = $cal.currentMonth;
                        currentYear = $cal.currentYear;
                }
        });
 
-       //#TODO Muss in die separate /read page (diese hier in /write umbenennen)
+       const imageExtensions = ['jpeg', 'jpg', 'gif', 'png', 'webp'];
+       //TODO: support svg? -> minsize is necessary...
+
+       // copy of files, which are images
+       $effect(() => {
+               if (logs) {
+                       logs.forEach((log) => {
+                               if (log.files) {
+                                       if (!log.images) {
+                                               log.images = [];
+                                       }
+
+                                       log.files.forEach((file) => {
+                                               if (
+                                                       imageExtensions.includes(file.filename.split('.').pop().toLowerCase()) &&
+                                                       !log.images.find((image) => image.uuid_filename === file.uuid_filename)
+                                               ) {
+                                                       log.images = [...log.images, file];
+
+                                                       if ($autoLoadImages) {
+                                                               loadImage(file.uuid_filename);
+                                                       }
+                                               }
+                                       });
+                               }
+                       });
+               }
+       });
+
+       function loadImage(uuid) {
+               for (let i = 0; i < logs.length; i++) {
+                       let log = logs[i];
+
+                       // skip log if file not in this day/log
+                       if (!log.images) {
+                               continue;
+                       }
+                       let image = log.images.find((image) => image.uuid_filename === uuid);
+                       if (!image) {
+                               continue;
+                       }
+
+                       log.images = log.images.map((image) => {
+                               if (image.uuid_filename === uuid) {
+                                       image.loading = true;
+                               }
+                               return image;
+                       });
+
+                       axios
+                               .get(API_URL + '/logs/downloadFile', {
+                                       params: { uuid: uuid },
+                                       responseType: 'blob',
+                                       signal: cancelDownload.signal
+                               })
+                               .then((response) => {
+                                       const url = URL.createObjectURL(new Blob([response.data]));
+                                       log.images = log.images.map((image) => {
+                                               if (image.uuid_filename === uuid) {
+                                                       image.src = url;
+                                                       image.loading = false;
+                                               }
+                                               return image;
+                                       });
+
+                                       log.files = log.files.map((file) => {
+                                               if (file.uuid_filename === uuid) {
+                                                       file.src = url;
+                                               }
+                                               return file;
+                                       });
+                               })
+                               .catch((error) => {
+                                       if (error.name == 'CanceledError') {
+                                               return;
+                                       }
+
+                                       console.error(error);
+                                       // toast
+                                       const toast = new bootstrap.Toast(document.getElementById('toastErrorLoadingFile'));
+                                       toast.show();
+                               });
+               }
+       }
+
+       function loadImages() {
+               for (let i = 0; i < logs.length; i++) {
+                       let log = logs[i];
+
+                       // skip log if no images in this day/log
+                       if (!log.images) {
+                               continue;
+                       }
+
+                       log.images.forEach((image) => {
+                               if (!image.src) {
+                                       loadImage(image.uuid_filename);
+                               }
+                       });
+               }
+       }
+
+       let cancelDownload = new AbortController();
+
+       function downloadFile(uuid) {
+               for (let i = 0; i < logs.length; i++) {
+                       let log = logs[i];
+
+                       // skip log if file not in this day/log
+                       if (!log.files) {
+                               continue;
+                       }
+                       let file = log.files.find((file) => file.uuid_filename === uuid);
+                       if (!file) {
+                               continue;
+                       }
+
+                       // check if src is present in files
+                       if (file.src) {
+                               triggerAutomaticDownload(uuid);
+                               return;
+                       }
+
+                       // otherwise: download from server
+                       log.files = log.files.map((f) => {
+                               if (f.uuid_filename === uuid) {
+                                       f.downloadProgress = 0;
+                               }
+                               return f;
+                       });
+
+                       const config = {
+                               params: { uuid: uuid },
+                               onDownloadProgress: (progressEvent) => {
+                                       log.files = log.files.map((file) => {
+                                               if (file.uuid_filename === uuid) {
+                                                       file.downloadProgress = Math.round((progressEvent.loaded / file.size) * 100);
+                                               }
+                                               return file;
+                                       });
+                               },
+                               signal: cancelDownload.signal,
+                               responseType: 'blob'
+                       };
+
+                       axios
+                               .get(API_URL + '/logs/downloadFile', {
+                                       ...config
+                               })
+                               .then((response) => {
+                                       const url = URL.createObjectURL(new Blob([response.data]));
+                                       log.files = log.files.map((f) => {
+                                               if (f.uuid_filename === uuid) {
+                                                       f.src = url;
+                                               }
+                                               return f;
+                                       });
+                               })
+                               .catch((error) => {
+                                       if (error.name == 'CanceledError') {
+                                               return;
+                                       }
+
+                                       console.error(error);
+                                       // toast
+                                       const toast = new bootstrap.Toast(document.getElementById('toastErrorLoadingFile'));
+                                       toast.show();
+                               })
+                               .finally(() => {
+                                       // remove progress
+                                       log.files = log.files.map((f) => {
+                                               if (f.uuid_filename === uuid) {
+                                                       f.downloadProgress = -1;
+                                               }
+                                               return f;
+                                       });
+
+                                       triggerAutomaticDownload(uuid);
+                               });
+               }
+       }
+
+       //#TODO Anpassen
+       function triggerAutomaticDownload(uuid) {
+               for (let i = 0; i < logs.length; i++) {
+                       let log = logs[i];
+
+                       // skip log if file not in this day/log
+                       if (!log.files) {
+                               continue;
+                       }
+                       let file = log.files.find((file) => file.uuid_filename === uuid);
+                       if (!file) {
+                               continue;
+                       }
+
+                       const a = document.createElement('a');
+                       a.href = file.src;
+                       a.download = file.filename;
+                       document.body.appendChild(a);
+                       a.click();
+                       document.body.removeChild(a);
+               }
+       }
+
        let isLoadingMonthForReading = false;
+
        function loadMonthForReading() {
                if (isLoadingMonthForReading) {
                        return;
        </div>
 
        <!-- Center -->
-       <div class="d-flex flex-column my-4 mx-4 flex-fill overflow-y-auto" id="scrollArea">
-               {#each logs as log}
+       <div class="d-flex flex-column my-4 ms-4 flex-fill overflow-y-auto" id="scrollArea">
+               {#each logs as log (log.day)}
                        <!-- Log-Area -->
                        <div class="log mb-3 p-3 d-flex flex-row" data-log-day={log.day}>
                                <div class="date me-3 d-flex flex-column align-items-center">
                                                </b>
                                        </p>
                                </div>
-                               <div>
-                                       {@html marked.parse(log.text)}
+                               <div class="flex-grow-1">
+                                       {#if log.text && log.text !== ''}
+                                               <div class="text">
+                                                       {@html marked.parse(log.text)}
+                                               </div>
+                                       {/if}
+                                       {#if log.tags?.length > 0}
+                                               <div class="tags d-flex flex-row flex-wrap">
+                                                       {#each log.tags as t}
+                                                               <Tag tag={$tags.find((tag) => tag.id === t)} />
+                                                       {/each}
+                                               </div>
+                                       {/if}
+                                       {#if log.images?.length > 0}
+                                               {#if !$autoLoadImages && log.images.find((image) => !image.src && !image.loading)}
+                                                       <div class="d-flex flex-row">
+                                                               <button type="button" class="loadImageBtn" onclick={() => loadImages()}>
+                                                                       <Fa icon={faCloudArrowDown} class="me-2" size="2x" fw /><br />
+                                                                       Bilder laden
+                                                               </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>
+                                               {/if}
+                                       {/if}
                                </div>
+
+                               {#if log.files && log.files.length > 0}
+                                       <div class="d-flex flex-column">
+                                               <FileList files={log.files} {downloadFile} />
+                                       </div>
+                               {/if}
                        </div>
                {/each}
        </div>
-
-       <div id="right">Right</div>
 </div>
 
 <style>
+       .loadImageBtn {
+               padding: 0.5rem 1rem;
+               border: none;
+               margin-top: 0.5rem;
+               border-radius: 5px;
+               transition: all ease 0.2s;
+               background-color: #ccc;
+       }
+
+       .loadImageBtn:hover {
+               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;
+       }
+
+       .tags {
+               gap: 0.5rem;
+       }
+
        .log {
                backdrop-filter: blur(10px) saturate(150%);
                background-color: rgba(199, 199, 201, 0.329);
        .dateDay {
                opacity: 0.7;
        }
+
+       #scrollArea {
+               padding-right: 1rem;
+       }
 </style>
index c834b8457bf9d8c36b76835dd4cb4a2420598fa2..61ec8a4422dce103f73859f3199775434fbe5924 100644 (file)
@@ -14,7 +14,6 @@
        import {
                faCloudArrowUp,
                faCloudArrowDown,
-               faTrash,
                faSquarePlus,
                faQuestionCircle
        } from '@fortawesome/free-solid-svg-icons';
@@ -25,6 +24,8 @@
        import { tags } from '$lib/tagStore';
        import Tag from '$lib/Tag.svelte';
        import TagModal from '$lib/TagModal.svelte';
+       import FileList from '$lib/FileList.svelte';
+       import { formatBytes } from '$lib/helpers.js';
 
        axios.interceptors.request.use((config) => {
                config.withCredentials = true;
                        });
        }
 
-       function formatBytes(bytes) {
-               if (!+bytes) return '0 Bytes';
-
-               const k = 1024;
-               //const dm = 2; // decimal places
-               const sizes = ['B', 'KB', 'MB', 'GB'];
-
-               const i = Math.floor(Math.log(bytes) / Math.log(k));
-
-               return `${parseFloat((bytes / Math.pow(k, i)).toFixed(0))} ${sizes[i]}`;
-       }
-
        function downloadFile(uuid) {
                // check if present in filesOfDay
                let file = filesOfDay.find((file) => file.uuid_filename === uuid);
                                filesOfDay = filesOfDay.map((file) => {
                                        if (file.uuid_filename === uuid) {
                                                file.downloadProgress = Math.round((progressEvent.loaded / file.size) * 100);
-                                               console.log(progressEvent);
                                        }
                                        return file;
                                });
                a.href = file.src;
                a.download = file.filename;
                document.body.appendChild(a);
-               console.log(a);
                a.click();
                document.body.removeChild(a);
        }
                        <div id="editor"></div>
                </div>
                {#if images.length > 0}
-                       {#if !$autoLoadImages && !images.find((image) => image.src || image.loading)}
+                       {#if !$autoLoadImages && images.find((image) => !image.src && !image.loading)}
                                <div class="d-flex flex-row">
-                                       <button type="button" id="loadImageBtn" onclick={() => loadImages()}>
+                                       <button type="button" class="loadImageBtn" onclick={() => loadImages()}>
                                                <Fa icon={faCloudArrowDown} class="me-2" size="2x" fw /><br />
                                                {#if images.length === 1}
                                                        1 Bild laden
                                                {:else}
                                                        {images.length} Bilder laden
                                                {/if}
+                                               ({formatBytes(
+                                                       images.filter((i) => !i.src).reduce((sum, image) => sum + (image.size || 0), 0)
+                                               )})
                                        </button>
                                </div>
                        {:else}
                        >
                        <input type="file" id="fileInput" multiple style="display: none;" onchange={onFileChange} />
 
-                       {#each filesOfDay as file (file.uuid_filename)}
-                               <div class="btn-group file mt-2" transition:slide>
-                                       <button
-                                               onclick={() => downloadFile(file.uuid_filename)}
-                                               class="p-2 fileBtn d-flex flex-column flex-fill"
-                                       >
-                                               <div class="d-flex flex-row align-items-center">
-                                                       <div class="filename filenameWeight">{file.filename}</div>
-                                                       <span class="filesize">({formatBytes(file.size)})</span>
-                                               </div>
-                                               {#if file.downloadProgress >= 0}
-                                                       <div
-                                                               class="progress"
-                                                               role="progressbar"
-                                                               aria-label="Download progress"
-                                                               aria-valuemin="0"
-                                                               aria-valuemax="100"
-                                                       >
-                                                               <div
-                                                                       class="progress-bar overflow-visible bg-info {file.downloadProgress === 0
-                                                                               ? 'progress-bar-striped progress-bar-animated'
-                                                                               : ''}"
-                                                                       style:width={file.downloadProgress + '%'}
-                                                                       aria-valuenow={file.downloadProgress}
-                                                                       aria-valuemax="100"
-                                                               >
-                                                                       {#if file.downloadProgress === 0}
-                                                                               <span class="text-dark">Wird entschlüsselt...</span>
-                                                                       {:else}
-                                                                               <span class="text-dark">Download: {file.downloadProgress}%</span>
-                                                                       {/if}
-                                                               </div>
-                                                       </div>
-                                               {/if}
-                                       </button>
-                                       <button
-                                               class="p-2 fileBtn deleteFileBtn"
-                                               onclick={() => askDeleteFile(file.uuid_filename, file.filename)}
-                                               ><Fa icon={faTrash} fw /></button
-                                       >
-                               </div>
-                       {/each}
+                       <FileList files={filesOfDay} {downloadFile} {askDeleteFile} deleteAllowed />
                        {#each uploadingFiles as file}
                                <div>
                                        {file.name}
                border-radius: 10px;
        }
 
-       #loadImageBtn {
+       .loadImageBtn {
                padding: 0.5rem 1rem;
                border: none;
                margin-top: 0.5rem;
                font-weight: 550;
        }
 
-       .filename {
-               padding-right: 0.5rem;
-               word-break: break-word;
-       }
-
-       .filesize {
-               opacity: 0.7;
-               font-size: 0.8rem;
-               white-space: nowrap;
-       }
-
-       .fileBtn {
-               border: 0;
-               background-color: rgba(0, 0, 0, 0);
-               transition: all ease 0.3s;
-       }
-
-       .fileBtn:hover {
-               background-color: rgba(0, 0, 0, 0.1);
-       }
-
-       .deleteFileBtn {
-               border-left: 1px solid rgba(92, 92, 92, 0.445);
-       }
-
-       .deleteFileBtn:hover {
-               color: rgb(165, 0, 0);
-       }
-
-       .file {
-               background-color: rgba(117, 117, 117, 0.45);
-               border: 0px solid #ececec77;
-               border-radius: 5px;
-       }
-
        .files {
                margin-right: 2rem;
                border-radius: 10px;
git clone https://git.99rst.org/PROJECT