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"])
--- /dev/null
+<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>
--- /dev/null
+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 };
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,
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>
import {
faCloudArrowUp,
faCloudArrowDown,
- faTrash,
faSquarePlus,
faQuestionCircle
} from '@fortawesome/free-solid-svg-icons';
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;