settings-logic is working
authorPhiTux <redacted>
Thu, 17 Apr 2025 17:28:04 +0000 (19:28 +0200)
committerPhiTux <redacted>
Thu, 17 Apr 2025 17:28:04 +0000 (19:28 +0200)
backend/server/main.py
backend/server/routers/users.py
backend/server/utils/fileHandling.py
backend/server/utils/settings.py
frontend/src/lib/settingsStore.js
frontend/src/routes/+layout.svelte
frontend/src/routes/write/+page.svelte

index 86afc1553f4cc388d4c17080d13fea9b4843ba2b..c117d0e524a6d078afc49b861c64bd9ed45b4493 100644 (file)
@@ -3,6 +3,7 @@ from .routers import users, logs
 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)
@@ -12,14 +13,7 @@ logger.setLevel(logging.DEBUG)
 
 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,
index 36c8b3d01bda23bb4c0c22e59af29f5edc0e1e88..aab3576bc85690717b4e9196d4246b11b918b645 100644 (file)
@@ -2,7 +2,7 @@ import asyncio
 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
@@ -122,6 +122,76 @@ async def register(register: Register):
     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,
+});
+"""
+
+
 
 """
 {
index 1873f62a28c16420b4b5631a10b203cfecd9b5cc..9f78290c5c071524913b132aeeee0aadf0fbc82c 100644 (file)
@@ -48,7 +48,7 @@ def writeUsers(content):
         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):
@@ -60,7 +60,7 @@ 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):
@@ -131,5 +131,30 @@ def writeTags(user_id, content):
         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
index b1e1fc9f145a7492906043a5487f871f75755f0b..5eb1691760d675954b5a269dfa28fb687277a8b6 100644 (file)
@@ -6,5 +6,7 @@ class Settings(BaseSettings):
   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
index ad8f715035b64c060e6a6a4468e4e6b47fe4dd7d..d22cd099006afcd969a408fe88088fd02b9596bc 100644 (file)
@@ -6,15 +6,9 @@ export const useTrianglify = writable(true);
 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
index 44afa21213742cd50d2e84b1e6a0385b74d9e543..fed04740c5a0c18d62b59df2714907abf11aacc2 100644 (file)
@@ -7,14 +7,21 @@
        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,
@@ -75,6 +82,8 @@
 
        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;
index e77d74c2ce3adbf80d746646c94d5472c40cbe3d..15e16b966b748043b9f1b4e21a8ae7fb088e9402 100644 (file)
@@ -20,7 +20,7 @@
        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">
git clone https://git.99rst.org/PROJECT