from fastapi.middleware.cors import CORSMiddleware
import logging
from sys import stdout
+from .utils.settings import settings
logger = logging.getLogger("dailytxtLogger")
consoleHandler = logging.StreamHandler(stdout)
app = FastAPI()
-origins = [
- "http://localhost:5173",
- "localhost:5173",
- "http://192.168.1.35:5173",
- "192.168.1.35:5173",
- "http://100.100.87.111:5173",
- "http://lab:5173"
-]
+origins = settings.allowed_hosts
app.add_middleware(
CORSMiddleware,
import datetime
import json
import secrets
-from fastapi import APIRouter, Cookie, HTTPException, Response
+from fastapi import APIRouter, Cookie, Depends, HTTPException, Response
from pydantic import BaseModel
from ..utils import fileHandling
from ..utils import security
else:
return {"success": True}
+def get_default_user_settings():
+ return {
+ "autoloadImagesByDefault": False,
+ "setAutoloadImagesPerDevice": True,
+ }
+
+@router.get("/getUserSettings")
+async def get_user_settings(cookie = Depends(isLoggedIn)):
+ user_id = cookie["user_id"]
+ content_enc = fileHandling.getUserSettings(user_id)
+
+ if len(content_enc) > 0:
+ # decrypt settings
+ enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+ content = json.loads(security.decrypt_text(content_enc, enc_key))
+ else:
+ content = {}
+
+ default = get_default_user_settings()
+
+ for key in default.keys():
+ if key not in content.keys():
+ content[key] = default[key]
+
+ return content
+
+
+@router.post("/saveUserSettings")
+async def save_user_settings(settings: dict, cookie = Depends(isLoggedIn)):
+ user_id = cookie["user_id"]
+ content = fileHandling.getUserSettings(user_id)
+
+ enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+ if len(content) > 0:
+ # decrypt settings
+ content = json.loads(security.decrypt_text(content, enc_key))
+ else:
+ content = {}
+
+ # if content is empty dict
+ if content is None or len(content) == 0:
+ content = get_default_user_settings()
+
+ # update settings
+ for key in settings.keys():
+ content[key] = settings[key]
+
+ # encrypt settings
+ content_enc = security.encrypt_text(json.dumps(content), enc_key)
+
+ try:
+ fileHandling.writeUserSettings(user_id, content_enc)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail="Internal Server Error when trying to write users.json") from e
+ else:
+ return {"success": True}
+
+
+"""
+export const defaultSettings = writable({
+ useTrianglify: true,
+ trianglifyOpacity: 0.4,
+ trianglifyColor: '',
+ backgroundColor: '',
+ autoloadImagesDefault: true,
+ saveAutoloadImagesPerDevice: true,
+});
+"""
+
+
"""
{
return e
else:
with f:
- f.write(json.dumps(content, indent=4))
+ f.write(json.dumps(content, indent=settings.indent))
return True
def writeMonth(user_id, year, month, content):
return False
else:
with f:
- f.write(json.dumps(content, indent=4))
+ f.write(json.dumps(content, indent=settings.indent))
return True
def get_years(user_id):
return False
else:
with f:
- f.write(json.dumps(content, indent=4))
+ f.write(json.dumps(content, indent=settings.indent))
+ return True
+
+def getUserSettings(user_id):
+ try:
+ f = open(os.path.join(settings.data_path, str(user_id), "settings.encrypted"), "r")
+ except FileNotFoundError:
+ logger.info(f"{user_id}/settings.encrypted - File not found")
+ return {}
+ except Exception as e:
+ logger.exception(e)
+ raise HTTPException(status_code=500, detail="Internal Server Error when trying to open settings.json")
+ else:
+ with f:
+ s = f.read()
+ return s
+
+def writeUserSettings(user_id, content):
+ try:
+ f = open(os.path.join(settings.data_path, str(user_id), "settings.encrypted"), "w")
+ except Exception as e:
+ logger.exception(e)
+ return False
+ else:
+ with f:
+ f.write(content)
return True
\ No newline at end of file
development: bool = False
secret_token: str = secrets.token_urlsafe(32)
logout_after_days: int = 30
+ allowed_hosts: list[str] = ["http://localhost:5173","http://127.0.0.1:5173"]
+ indent: int | None = None
settings = Settings()
\ No newline at end of file
export const trianglifyOpacity = writable(0.4);
export const trianglifyColor = writable('');
export const backgroundColor = writable('');
-export const autoLoadImages = writable(true);
-export const settings = writable({
- useTrianglify: true,
- trianglifyOpacity: 0.4,
- trianglifyColor: '',
- backgroundColor: '',
- autoloadImagesDefault: true,
- saveAutoloadImagesPerDevice: true,
-});
+export const settings = writable({});
-export const tempSettings = writable({});
\ No newline at end of file
+export const tempSettings = writable({});
+
+export const autoLoadImagesThisDevice = writable(JSON.parse(localStorage.getItem('autoLoadImagesThisDevice')));
\ No newline at end of file
import '../scss/styles.scss';
import * as bootstrap from 'bootstrap';
import Fa from 'svelte-fa';
- import { readingMode } from '$lib/settingsStore.js';
+ import {
+ readingMode,
+ settings,
+ tempSettings,
+ autoLoadImagesThisDevice,
+ useTrianglify,
+ trianglifyOpacity
+ } from '$lib/settingsStore.js';
import { page } from '$app/state';
import { API_URL } from '$lib/APIurl.js';
import trianglify from 'trianglify';
- import { useTrianglify, trianglifyOpacity, autoLoadImages } from '$lib/settingsStore.js';
import { tags } from '$lib/tagStore.js';
import TagModal from '$lib/TagModal.svelte';
import { alwaysShowSidenav } from '$lib/helpers.js';
+ import { slide } from 'svelte/transition';
import {
faRightFromBracket,
let settingsModal;
function openSettingsModal() {
+ $tempSettings = JSON.parse(JSON.stringify($settings));
+
settingsModal = new bootstrap.Modal(document.getElementById('settingsModal'));
settingsModal.show();
}
calculateResize();
});
+ function getUserSettings() {
+ axios
+ .get(API_URL + '/users/getUserSettings')
+ .then((response) => {
+ $settings = response.data;
+ })
+ .catch((error) => {
+ console.error(error);
+ })
+ .finally(() => {
+ if ($autoLoadImagesThisDevice === null || $autoLoadImagesThisDevice === undefined) {
+ $autoLoadImagesThisDevice = $settings.autoloadImagesByDefault;
+ }
+ });
+ }
+
+ let isSaving = $state(false);
+ function saveUserSettings() {
+ if (isSaving) return;
+ isSaving = true;
+
+ axios
+ .post(API_URL + '/users/saveUserSettings', $tempSettings)
+ .then((response) => {
+ if (response.data.success) {
+ $settings = $tempSettings;
+
+ // show toast
+ const toast = new bootstrap.Toast(document.getElementById('toastSuccessSaveSettings'));
+ toast.show();
+
+ settingsModal.hide();
+ } else {
+ throw new Error('Error saving settings');
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+
+ // show toast
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorSaveSettings'));
+ toast.show();
+ })
+ .finally(() => {
+ isSaving = false;
+ });
+ }
+
onMount(() => {
createBackground();
calculateResize();
+ getUserSettings();
document.getElementById('settingsModal').addEventListener('shown.bs.modal', function () {
const height = document.getElementById('modal-body').clientHeight;
.finally(() => {
isSavingEditedTag = false;
editTagModal.close();
- settingsModal.show();
+ openSettingsModal();
});
}
+
+ $effect(() => {
+ if ($autoLoadImagesThisDevice === null || $autoLoadImagesThisDevice === undefined) {
+ return;
+ }
+
+ localStorage.setItem('autoLoadImagesThisDevice', $autoLoadImagesThisDevice);
+ });
</script>
<main class="d-flex flex-column">
<h3 id="functions" class="text-primary">Funktionen</h3>
<div id="autoLoadImages">
+ {#if $tempSettings.setAutoloadImagesPerDevice !== $settings.setAutoloadImagesPerDevice || $tempSettings.autoloadImagesByDefault !== $settings.autoloadImagesByDefault}
+ <div class="unsaved-changes" transition:slide></div>
+ {/if}
+
<h5>Bilder automatisch laden</h5>
<ul>
<li>
<div class="form-check form-switch">
<input
class="form-check-input"
- bind:checked={$autoLoadImages}
+ bind:checked={$tempSettings.setAutoloadImagesPerDevice}
+ type="checkbox"
+ role="switch"
+ id="setImageLoadingPerDeviceSwitch"
+ />
+ <label class="form-check-label" for="setImageLoadingPerDeviceSwitch">
+ Für jedes Gerät einzeln festlegen, ob die Bilder automatisch geladen werden
+ sollen</label
+ >
+ </div>
+
+ <div class="form-check form-switch ms-3">
+ <input
+ class="form-check-input"
+ bind:checked={$autoLoadImagesThisDevice}
+ type="checkbox"
+ role="switch"
+ id="loadImagesThisDeviceSwitch"
+ disabled={!$tempSettings.setAutoloadImagesPerDevice}
+ />
+ <label class="form-check-label" for="loadImagesThisDeviceSwitch">
+ {#if $autoLoadImagesThisDevice}
+ Bilder werden auf <b>diesem Gerät</b> automatisch geladen
+ {:else}
+ Bilder werden auf <b>diesem Gerät <u>nicht</u></b> automatisch geladen
+ {/if}</label
+ >
+ </div>
+
+ <div class="form-check form-switch mt-3">
+ <input
+ class="form-check-input"
+ bind:checked={$tempSettings.autoloadImagesByDefault}
type="checkbox"
role="switch"
id="autoLoadImagesSwitch"
+ disabled={$tempSettings.setAutoloadImagesPerDevice}
/>
<label class="form-check-label" for="autoLoadImagesSwitch">
- {#if $autoLoadImages}
- Bilder werden automatisch geladen
+ {#if $tempSettings.autoloadImagesByDefault}
+ Bilder werden (auf jedem Gerät) automatisch geladen
{:else}
- Bilder werden nicht automatisch geladen
+ Bilder werden (auf jedem Gerät) <b>nicht</b> automatisch geladen
{/if}</label
>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
- <button type="button" class="btn btn-primary">Speichern</button>
+ <button
+ type="button"
+ class="btn btn-primary"
+ onclick={saveUserSettings}
+ disabled={isSaving}
+ >Speichern
+ {#if isSaving}
+ <span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
</div>
</div>
</div>
<div class="toast-body">Fehler beim Löschen des Tags!</div>
</div>
</div>
+
+ <div
+ id="toastSuccessSaveSettings"
+ class="toast align-items-center text-bg-success"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Einstellungen gespeichert!</div>
+ </div>
+ </div>
+
+ <div
+ id="toastErrorSaveSettings"
+ class="toast align-items-center text-bg-danger"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Fehler beim Speichern der Einstellungen!</div>
+ </div>
+ </div>
</div>
</main>
<style>
+ div:has(> .unsaved-changes) {
+ outline: 1px solid orange;
+ }
+
+ .unsaved-changes {
+ background-color: orange;
+ margin-top: -0.5rem;
+ margin-left: -0.5rem;
+ margin-right: -0.5rem;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ padding-left: 0.5rem;
+ margin-bottom: 0.5rem;
+ }
+
+ .unsaved-changes::before {
+ content: 'Ungespeicherte Änderungen';
+ }
+
:global(.tagColumn > span) {
width: min-content;
}
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255, 255, 255, 1)'/></svg>");
}
+ .settings-content {
+ padding: 0.5rem;
+ }
+
#settings-content > div {
background-color: #bdbdbd5d;
padding: 0.5rem;
import Fa from 'svelte-fa';
import { v4 as uuidv4 } from 'uuid';
import { slide, fade } from 'svelte/transition';
- import { autoLoadImages } from '$lib/settingsStore';
+ import { settings, autoLoadImagesThisDevice } from '$lib/settingsStore';
import { tags } from '$lib/tagStore';
import Tag from '$lib/Tag.svelte';
import TagModal from '$lib/TagModal.svelte';
) {
images = [...images, file];
- if ($autoLoadImages) {
+ if (autoLoadImages) {
loadImage(file);
}
}
}
});
+ let autoLoadImages = $derived(
+ ($settings.setAutoloadImagesPerDevice && $autoLoadImagesThisDevice) ||
+ (!$settings.setAutoloadImagesPerDevice && $settings.autoloadImagesByDefault)
+ );
+
function loadImage(file) {
images.map((image) => {
if (image.uuid_filename === file.uuid_filename) {
<!-- Center -->
<div class="d-flex flex-column pt-4 px-4 flex-fill" id="middle">
<!-- Input-Area -->
- <!-- <div class="d-flex flex-column"> -->
<div class="d-flex flex-row textAreaHeader">
<div class="flex-fill textAreaDate">
{$selectedDate.toLocaleDateString('locale', { weekday: 'long' })}<br />
<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" class="loadImageBtn" onclick={() => loadImages()}>
<Fa icon={faCloudArrowDown} class="me-2" size="2x" fw /><br />
</div>
{:else}
<ImageViewer {images} />
- <!-- <div class="d-flex flex-row images mt-3">
- {#each 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> -->
</div>
<div id="right" class="d-flex flex-column">