added template-logic (except when adding in editor)
authorPhiTux <redacted>
Fri, 18 Apr 2025 17:22:41 +0000 (19:22 +0200)
committerPhiTux <redacted>
Fri, 18 Apr 2025 17:22:41 +0000 (19:22 +0200)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/src/lib/settingsStore.js
frontend/src/lib/templateStore.js [new file with mode: 0644]
frontend/src/routes/+layout.svelte

index 2616e0ef16f5f170da13302267ce2b26931dfc28..24904d347db06728a4d4e7e9702fda45969781aa 100644 (file)
@@ -520,4 +520,39 @@ async def removeTagFromLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)
         if not fileHandling.writeMonth(cookie["user_id"], data.year, data.month, content):
             raise HTTPException(status_code=500, detail="Failed to remove tag - error writing log")
         return {"success": True}
-    
\ No newline at end of file
+    
+@router.get("/getTemplates")
+async def getTemplates(cookie = Depends(users.isLoggedIn)):
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    
+    content:dict = fileHandling.getTemplates(cookie["user_id"])
+
+    if not 'templates' in content:
+        return []
+    
+    else:
+        for template in content['templates']:
+            template['name'] = security.decrypt_text(template['name'], enc_key)
+            template['text'] = security.decrypt_text(template['text'], enc_key)
+        return content['templates']
+
+class Templates(BaseModel):
+    templates: list[dict]
+
+@router.post("/saveTemplates")
+async def saveTemplates(templates: Templates, cookie = Depends(users.isLoggedIn)):
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    
+    content = {'templates': []}
+    
+    for template in templates.templates:
+        enc_name = security.encrypt_text(template["name"], enc_key)
+        enc_text = security.encrypt_text(template["text"], enc_key)
+
+        new_template = {"name": enc_name, "text": enc_text}
+        content['templates'].append(new_template)
+
+    if not fileHandling.writeTemplates(cookie["user_id"], content):
+        raise HTTPException(status_code=500, detail="Failed to write templates - error writing templates")
+    else:
+        return {"success": True}
\ No newline at end of file
index 9f78290c5c071524913b132aeeee0aadf0fbc82c..954e0b8bc9d8e84ab413d230bcc9d8377fdfafdb 100644 (file)
@@ -157,4 +157,31 @@ def writeUserSettings(user_id, content):
     else:
         with f:
             f.write(content)
+            return True
+        
+def getTemplates(user_id):
+    try:
+        f = open(os.path.join(settings.data_path, str(user_id), "templates.json"), "r")
+    except FileNotFoundError:
+        logger.info(f"{user_id}/templates.json - File not found")
+        return {}
+    except Exception as e:
+        logger.exception(e)
+        raise HTTPException(status_code=500, detail="Internal Server Error when trying to open templates.json")
+    else:
+        with f:
+            s = f.read()
+            if s == "":
+                return {}
+            return json.loads(s)
+        
+def writeTemplates(user_id, content):
+    try:
+        f = open(os.path.join(settings.data_path, str(user_id), "templates.json"), "w")
+    except Exception as e:
+        logger.exception(e)
+        return False
+    else:
+        with f:
+            f.write(json.dumps(content, indent=settings.indent))
             return True
\ No newline at end of file
index d22cd099006afcd969a408fe88088fd02b9596bc..e052facddb613efb5fd53497c1459227b8217e7d 100644 (file)
@@ -2,13 +2,16 @@ import {writable} from 'svelte/store';
 
 export const readingMode = writable(false);
 
+// old, to be deleted
 export const useTrianglify = writable(true);
 export const trianglifyOpacity = writable(0.4);
 export const trianglifyColor = writable('');
 export const backgroundColor = writable('');
+//=========
 
 export const settings = writable({});
 
 export const tempSettings = writable({});
 
+// should be separate, since it interacts with localStorage
 export const autoLoadImagesThisDevice = writable(JSON.parse(localStorage.getItem('autoLoadImagesThisDevice')));
\ No newline at end of file
diff --git a/frontend/src/lib/templateStore.js b/frontend/src/lib/templateStore.js
new file mode 100644 (file)
index 0000000..6f04ae5
--- /dev/null
@@ -0,0 +1,3 @@
+import { writable } from "svelte/store";
+
+export const templates = writable([]);
\ No newline at end of file
index fed04740c5a0c18d62b59df2714907abf11aacc2..807a541f7088417e3a99ddbc8d91e12fe75778bb 100644 (file)
@@ -1,5 +1,5 @@
 <script>
-       import { blur } from 'svelte/transition';
+       import { blur, slide, fade } from 'svelte/transition';
        import axios from 'axios';
        //import { dev } from '$app/environment';
        import { goto } from '$app/navigation';
        import { tags } from '$lib/tagStore.js';
        import TagModal from '$lib/TagModal.svelte';
        import { alwaysShowSidenav } from '$lib/helpers.js';
-       import { slide } from 'svelte/transition';
+       import { templates } from '$lib/templateStore';
 
        import {
                faRightFromBracket,
                faGlasses,
                faPencil,
                faSliders,
-               faTriangleExclamation
+               faTriangleExclamation,
+               faTrash
        } from '@fortawesome/free-solid-svg-icons';
        import Tag from '$lib/Tag.svelte';
 
                createBackground();
                calculateResize();
                getUserSettings();
+               getTemplates();
 
                document.getElementById('settingsModal').addEventListener('shown.bs.modal', function () {
                        const height = document.getElementById('modal-body').clientHeight;
                editTagModal.open();
        }
 
+       let selectedTemplate = $state(null);
+       let templateName = $state('');
+       let templateText = $state('');
+       let oldTemplateName = $state('');
+       let oldTemplateText = $state('');
+       let confirmDeleteTemplate = $state(false);
+
+       function getTemplates() {
+               axios
+                       .get(API_URL + '/logs/getTemplates')
+                       .then((response) => {
+                               $templates = response.data;
+
+                               // add "new template" option to start
+                               $templates.unshift({ name: 'Neue Vorlage erstellen...', text: '' });
+
+                               selectedTemplate = null;
+                               updateSelectedTemplate();
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                       });
+       }
+
+       let isSavingTemplate = $state(false);
+       function saveTemplate() {
+               // check if name or text is empty
+               if (templateName === '' || templateText === '') {
+                       // show toast
+                       const toast = new bootstrap.Toast(document.getElementById('toastErrorInvalidTemplateEmpty'));
+                       toast.show();
+                       return;
+               }
+
+               // check if template name already exists
+               for (let i = 0; i < $templates.length; i++) {
+                       if ($templates[i].name === templateName && selectedTemplate !== i) {
+                               // show toast
+                               const toast = new bootstrap.Toast(
+                                       document.getElementById('toastErrorInvalidTemplateDouble')
+                               );
+                               toast.show();
+                               return;
+                       }
+               }
+
+               if (isSavingTemplate) return;
+               isSavingTemplate = true;
+
+               if (selectedTemplate === 0) {
+                       // add new template
+                       $templates.push({ name: templateName, text: templateText });
+               } else {
+                       // update existing template
+                       $templates[selectedTemplate].name = templateName;
+                       $templates[selectedTemplate].text = templateText;
+               }
+
+               // remove first "new template" option
+               $templates.shift();
+
+               axios
+                       .post(API_URL + '/logs/saveTemplates', {
+                               templates: $templates
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       getTemplates();
+
+                                       // show toast
+                                       const toast = new bootstrap.Toast(document.getElementById('toastSuccessSaveTemplate'));
+                                       toast.show();
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+
+                               // show toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorSaveTemplates'));
+                               toast.show();
+                       })
+                       .finally(() => {
+                               isSavingTemplate = false;
+                       });
+       }
+
+       let isDeletingTemplate = $state(false);
+       function deleteTemplate() {
+               if (selectedTemplate === null || selectedTemplate === 0) return;
+
+               if (isDeletingTemplate) return;
+               isDeletingTemplate = true;
+
+               // remove template from list
+               $templates.splice(selectedTemplate, 1);
+
+               // remove first "new template" option
+               $templates.shift();
+
+               axios
+                       .post(API_URL + '/logs/saveTemplates', {
+                               templates: $templates
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       getTemplates();
+
+                                       // show toast
+                                       const toast = new bootstrap.Toast(
+                                               document.getElementById('toastSuccessDeletingTemplate')
+                                       );
+                                       toast.show();
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               // show toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorDeletingTemplate'));
+                               toast.show();
+                       })
+                       .finally(() => {
+                               isDeletingTemplate = false;
+                               confirmDeleteTemplate = false;
+                       });
+       }
+
+       function updateSelectedTemplate() {
+               if (selectedTemplate === 0 || selectedTemplate === null) {
+                       // new template
+                       templateName = '';
+                       templateText = '';
+               } else {
+                       // existing template
+                       templateName = $templates[selectedTemplate].name;
+                       templateText = $templates[selectedTemplate].text;
+               }
+               oldTemplateName = templateName;
+               oldTemplateText = templateText;
+
+               confirmDeleteTemplate = false;
+       }
+
        let deleteTagId = $state(null);
        function askDeleteTag(tagId) {
                if (deleteTagId === tagId) deleteTagId = null;
                                                                data-bs-smooth-scroll="true"
                                                                id="settings-content"
                                                        >
-                                                               <h3 id="appearance" class="text-primary">Aussehen</h3>
-                                                               <div id="lightdark">
-                                                                       <h5>Light/Dark-Mode</h5>
-                                                                       Bla<br />
-                                                                       blub <br />
-                                                                       bla <br />
-                                                                       blub <br />
-                                                               </div>
-                                                               <div id="background">
-                                                                       <h5>Hintergrund</h5>
-                                                                       <div class="d-flex flex-row justify-content-start">
-                                                                               <label for="trianglifyOpacity" class="form-label"
-                                                                                       >Transparenz der Dreiecke</label
-                                                                               >
-                                                                               <input
-                                                                                       bind:value={$trianglifyOpacity}
-                                                                                       type="range"
-                                                                                       class="mx-3 form-range"
-                                                                                       id="trianglifyOpacity"
-                                                                                       min="0"
-                                                                                       max="1"
-                                                                                       step="0.01"
-                                                                               />
-                                                                               <input
-                                                                                       bind:value={$trianglifyOpacity}
-                                                                                       type="number"
-                                                                                       id="trianglifyOpacityNumber"
-                                                                               />
+                                                               <div id="appearance">
+                                                                       <h3 id="" class="text-primary">Aussehen</h3>
+                                                                       <div id="lightdark">
+                                                                               <h5>Light/Dark-Mode</h5>
+                                                                               Bla<br />
+                                                                               blub <br />
+                                                                               bla <br />
+                                                                               blub <br />
+                                                                       </div>
+                                                                       <div id="background">
+                                                                               <h5>Hintergrund</h5>
+                                                                               <div class="d-flex flex-row justify-content-start">
+                                                                                       <label for="trianglifyOpacity" class="form-label"
+                                                                                               >Transparenz der Dreiecke</label
+                                                                                       >
+                                                                                       <input
+                                                                                               bind:value={$trianglifyOpacity}
+                                                                                               type="range"
+                                                                                               class="mx-3 form-range"
+                                                                                               id="trianglifyOpacity"
+                                                                                               min="0"
+                                                                                               max="1"
+                                                                                               step="0.01"
+                                                                                       />
+                                                                                       <input
+                                                                                               bind:value={$trianglifyOpacity}
+                                                                                               type="number"
+                                                                                               id="trianglifyOpacityNumber"
+                                                                                       />
+                                                                               </div>
                                                                        </div>
                                                                </div>
 
-                                                               <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>
-                                                                                       Beim Laden eines Textes können hochgeladene Bilder (sofern vorhanden)
-                                                                                       automatisch geladen werden. <em>Erhöhter Datenverbrauch!</em>
-                                                                               </li>
-                                                                               <li>Alternativ wird ein Button zum Nachladen aller Bilder angezeigt.</li>
-                                                                       </ul>
-
-                                                                       <div class="form-check form-switch">
-                                                                               <input
-                                                                                       class="form-check-input"
-                                                                                       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 id="functions">
+                                                                       <h3 id="" 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>
+                                                                                               Beim Laden eines Textes können hochgeladene Bilder (sofern vorhanden)
+                                                                                               automatisch geladen werden. <em>Erhöhter Datenverbrauch!</em>
+                                                                                       </li>
+                                                                                       <li>Alternativ wird ein Button zum Nachladen aller Bilder angezeigt.</li>
+                                                                               </ul>
+
+                                                                               <div class="form-check form-switch">
+                                                                                       <input
+                                                                                               class="form-check-input"
+                                                                                               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 $tempSettings.autoloadImagesByDefault}
+                                                                                                       Bilder werden (auf jedem Gerät) automatisch geladen
+                                                                                               {:else}
+                                                                                                       Bilder werden (auf jedem Gerät) <b>nicht</b> automatisch geladen
+                                                                                               {/if}</label
+                                                                                       >
+                                                                               </div>
                                                                        </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 id="language">
+                                                                               <h5>Sprache</h5>
+                                                                               Bla<br />
+                                                                               blub <br />
+                                                                               bla <br />
+                                                                               blub <br />
                                                                        </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 $tempSettings.autoloadImagesByDefault}
-                                                                                               Bilder werden (auf jedem Gerät) automatisch geladen
-                                                                                       {:else}
-                                                                                               Bilder werden (auf jedem Gerät) <b>nicht</b> automatisch geladen
-                                                                                       {/if}</label
-                                                                               >
+                                                                       <div id="timezone">
+                                                                               <h5>Zeitzone</h5>
+                                                                               Bla<br />
+                                                                               blub <br />
+                                                                               bla <br />
+                                                                               blub <br />
+                                                                       </div>
+                                                                       <div id="onthisday">
+                                                                               <h5>An diesem Tag</h5>
+                                                                               Bla<br />
+                                                                               blub <br />
+                                                                               bla <br />
+                                                                               blub <br />
+                                                                       </div>
+                                                                       <div id="loginonreload">
+                                                                               <h5>Login bei Reload</h5>
+                                                                               Bla<br />
+                                                                               blub <br />
+                                                                               bla <br />
+                                                                               blub <br />
                                                                        </div>
-                                                               </div>
-
-                                                               <div id="language">
-                                                                       <h5>Sprache</h5>
-                                                                       Bla<br />
-                                                                       blub <br />
-                                                                       bla <br />
-                                                                       blub <br />
-                                                               </div>
-                                                               <div id="timezone">
-                                                                       <h5>Zeitzone</h5>
-                                                                       Bla<br />
-                                                                       blub <br />
-                                                                       bla <br />
-                                                                       blub <br />
-                                                               </div>
-                                                               <div id="onthisday">
-                                                                       <h5>An diesem Tag</h5>
-                                                                       Bla<br />
-                                                                       blub <br />
-                                                                       bla <br />
-                                                                       blub <br />
-                                                               </div>
-                                                               <div id="loginonreload">
-                                                                       <h5>Login bei Reload</h5>
-                                                                       Bla<br />
-                                                                       blub <br />
-                                                                       bla <br />
-                                                                       blub <br />
                                                                </div>
 
                                                                <h3 id="tags" class="text-primary">Tags</h3>
                                                                        </div>
                                                                </div>
 
-                                                               <div id="templates"><h4>Vorlagen</h4></div>
+                                                               <div id="templates">
+                                                                       <h3 class="text-primary">Vorlagen</h3>
+                                                                       <div>
+                                                                               {#if oldTemplateName !== templateName || oldTemplateText !== templateText}
+                                                                                       <div class="unsaved-changes" transition:slide></div>
+                                                                               {/if}
+
+                                                                               <div class="d-flex flex-column">
+                                                                                       <select
+                                                                                               bind:value={selectedTemplate}
+                                                                                               class="form-select"
+                                                                                               aria-label="Select template"
+                                                                                               onchange={updateSelectedTemplate}
+                                                                                       >
+                                                                                               {#each $templates as template, index}
+                                                                                                       <option value={index} selected={index === selectedTemplate}>
+                                                                                                               {template.name}
+                                                                                                       </option>
+                                                                                               {/each}
+                                                                                       </select>
+                                                                               </div>
+
+                                                                               <hr />
+
+                                                                               {#if confirmDeleteTemplate}
+                                                                                       <div transition:slide class="d-flex flex-row align-items-center mb-2">
+                                                                                               <span
+                                                                                                       >Vorlage <b>{$templates[selectedTemplate].name}</b> wirklich löschen?</span
+                                                                                               >
+                                                                                               <button
+                                                                                                       type="button"
+                                                                                                       class="btn btn-secondary ms-2"
+                                                                                                       onclick={() => (confirmDeleteTemplate = false)}>Abbrechen</button
+                                                                                               >
+                                                                                               <button
+                                                                                                       type="button"
+                                                                                                       class="btn btn-danger ms-2"
+                                                                                                       onclick={() => {
+                                                                                                               deleteTemplate();
+                                                                                                       }}
+                                                                                                       disabled={isDeletingTemplate}
+                                                                                                       >Löschen
+                                                                                                       {#if isDeletingTemplate}
+                                                                                                               <span
+                                                                                                                       class="spinner-border spinner-border-sm ms-2"
+                                                                                                                       role="status"
+                                                                                                                       aria-hidden="true"
+                                                                                                               ></span>
+                                                                                                       {/if}
+                                                                                               </button>
+                                                                                       </div>
+                                                                               {/if}
+                                                                               <div class="d-flex flex-row">
+                                                                                       <input
+                                                                                               disabled={selectedTemplate === null}
+                                                                                               type="text"
+                                                                                               bind:value={templateName}
+                                                                                               class="form-control"
+                                                                                               placeholder="Name der Vorlage"
+                                                                                       />
+                                                                                       <button
+                                                                                               disabled={selectedTemplate === 0 || selectedTemplate === null}
+                                                                                               type="button"
+                                                                                               class="btn btn-outline-danger ms-5"
+                                                                                               onclick={() => {
+                                                                                                       confirmDeleteTemplate = !confirmDeleteTemplate;
+                                                                                               }}><Fa fw icon={faTrash} /></button
+                                                                                       >
+                                                                               </div>
+                                                                               <textarea
+                                                                                       disabled={selectedTemplate === null}
+                                                                                       bind:value={templateText}
+                                                                                       class="form-control mt-2"
+                                                                                       rows="10"
+                                                                                       placeholder="Inhalt der Vorlage"
+                                                                               >
+                                                                               </textarea>
+                                                                               <div class="d-flex justify-content-end">
+                                                                                       <button
+                                                                                               disabled={(oldTemplateName === templateName &&
+                                                                                                       oldTemplateText === templateText) ||
+                                                                                                       isSavingTemplate}
+                                                                                               type="button"
+                                                                                               class="btn btn-primary mt-2"
+                                                                                               onclick={saveTemplate}
+                                                                                       >
+                                                                                               Vorlage speichern
+                                                                                               {#if isSavingTemplate}
+                                                                                                       <span
+                                                                                                               class="spinner-border spinner-border-sm ms-2"
+                                                                                                               role="status"
+                                                                                                               aria-hidden="true"
+                                                                                                       ></span>
+                                                                                               {/if}
+                                                                                       </button>
+                                                                               </div>
+                                                                       </div>
+                                                               </div>
 
                                                                <div id="data">
                                                                        <h4>Daten</h4>
                                        </div>
                                </div>
                                <div class="modal-footer">
+                                       {#if JSON.stringify($tempSettings) !== JSON.stringify($settings)}
+                                               <div class="footer-unsaved-changes" transition:fade={{ duration: 100 }}>
+                                                       Ungespeicherte Änderungen!
+                                               </div>
+                                       {/if}
                                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
                                        <button
                                                type="button"
                                                class="btn btn-primary"
                                                onclick={saveUserSettings}
-                                               disabled={isSaving}
+                                               disabled={isSaving || JSON.stringify($tempSettings) === JSON.stringify($settings)}
                                                >Speichern
                                                {#if isSaving}
                                                        <span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"
                                <div class="toast-body">Fehler beim Speichern der Einstellungen!</div>
                        </div>
                </div>
+
+               <div
+                       id="toastErrorInvalidTemplateEmpty"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Name oder Inhalt einer Vorlage dürfen nicht leer sein!</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorInvalidTemplateDouble"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Name der Vorlage existiert bereits</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastSuccessSaveTemplate"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Vorlage gespeichert</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorDeletingTemplate"
+                       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 Löschen der Vorlage</div>
+                       </div>
+               </div>
+
+               <div
+                       id="toastSuccessDeletingTemplate"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">Vorlage gelöscht</div>
+                       </div>
+               </div>
        </div>
 </main>
 
 <style>
+       .footer-unsaved-changes {
+               background-color: orange;
+               color: black;
+               padding: 0.25rem 0.5rem;
+               border-radius: 10px;
+               margin-left: auto;
+               margin-right: 2rem;
+               font-style: italic;
+       }
+
        div:has(> .unsaved-changes) {
                outline: 1px solid orange;
        }
                padding: 0.5rem;
        }
 
-       #settings-content > div {
+       #settings-content > div > div {
                background-color: #bdbdbd5d;
                padding: 0.5rem;
                border-radius: 10px;
git clone https://git.99rst.org/PROJECT