progress in tags-handling
authorPhiTux <redacted>
Thu, 6 Mar 2025 22:36:18 +0000 (23:36 +0100)
committerPhiTux <redacted>
Thu, 6 Mar 2025 22:36:18 +0000 (23:36 +0100)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/package-lock.json
frontend/package.json
frontend/src/lib/Tag.svelte
frontend/src/lib/TagModal.svelte [new file with mode: 0644]
frontend/src/routes/+layout.svelte
frontend/src/routes/write/+page.svelte

index 839ea77cefec44bcdab6800a2eb8ab0eb104ca16..ba1b6a7634c2622c51c95b1f43854c83dc9fbe32 100644 (file)
@@ -331,12 +331,45 @@ async def downloadFile(uuid: str, cookie = Depends(users.isLoggedIn)):
 
 @router.get("/getTags")
 async def getTags(cookie = Depends(users.isLoggedIn)):
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    
     content:dict = fileHandling.getTags(cookie["user_id"])
 
     if not 'tags' in content:
         return []
     
     else:
+        for tag in content['tags']:
+            tag['icon'] = security.decrypt_text(tag['icon'], enc_key)
+            tag['name'] = security.decrypt_text(tag['name'], enc_key)
+            tag['color'] = security.decrypt_text(tag['color'], enc_key)
         return content['tags']
 
-    ### NOCH ENTSCHLÜSSELN!!!!
\ No newline at end of file
+
+class NewTag(BaseModel):
+    icon: str
+    name: str
+    color: str
+
+@router.post("/saveTag")
+async def saveTag(tag: NewTag, cookie = Depends(users.isLoggedIn)):
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    
+    content:dict = fileHandling.getTags(cookie["user_id"])
+    
+    if not 'tags' in content:
+        content['tags'] = []
+        content['next_id'] = 1
+    
+    enc_icon = security.encrypt_text(tag.icon, enc_key)
+    enc_name = security.encrypt_text(tag.name, enc_key)
+    enc_color = security.encrypt_text(tag.color, enc_key)
+
+    new_tag = {"id": content['next_id'], "icon": enc_icon, "name": enc_name, "color": enc_color}
+    content['next_id'] += 1
+    content['tags'].append(new_tag)
+
+    if not fileHandling.writeTags(cookie["user_id"], content):
+        return {"success": False}
+    else:
+        return {"success": True}
\ No newline at end of file
index f2ccef88c6cf16fc8145cf66cc44d1608b4af9ae..67410f5d5ff57a75e464e75ddd7b57df005c4470 100644 (file)
@@ -121,4 +121,15 @@ def getTags(user_id):
             s = f.read()
             if s == "":
                 return {}
-            return json.loads(s)
\ No newline at end of file
+            return json.loads(s)
+        
+def writeTags(user_id, content):
+    try:
+        f = open(os.path.join(settings.data_path, str(user_id), "tags.json"), "w")
+    except Exception as e:
+        logger.exception(e)
+        return False
+    else:
+        with f:
+            f.write(json.dumps(content, indent=4))
+            return True
\ No newline at end of file
index e8ddcf76db22899774b02f2f2116ad5708f40d0a..db5a44dd04f2a498ce81000d38903ed91c6bbef7 100644 (file)
@@ -13,6 +13,7 @@
                                "axios": "^1.7.8",
                                "bootstrap": "^5.3.3",
                                "dayjs": "^1.11.13",
+                               "emoji-picker-element": "^1.26.1",
                                "marked": "^15.0.6",
                                "svelte-outside": "^0.0.3",
                                "tiny-markdown-editor": "^0.1.31",
                        "dev": true,
                        "license": "MIT"
                },
+               "node_modules/emoji-picker-element": {
+                       "version": "1.26.1",
+                       "resolved": "https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.26.1.tgz",
+                       "integrity": "sha512-XgQ9s2JdmworiqLfJC7eGbzQHGv8yb8U9XofjeRAnOMYaeLh0MfwVAz9oG1YE2U2WnzU0Pys1axMjYtPKJ7YSg==",
+                       "license": "Apache-2.0"
+               },
                "node_modules/emoji-regex": {
                        "version": "8.0.0",
                        "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
index f50dac44c4d4a6aa8b71d915ece209c073a1d610..dcef1be9d3c3c209bc7e5e8ae8e53dc81ace0660 100644 (file)
@@ -37,6 +37,7 @@
                "axios": "^1.7.8",
                "bootstrap": "^5.3.3",
                "dayjs": "^1.11.13",
+               "emoji-picker-element": "^1.26.1",
                "marked": "^15.0.6",
                "svelte-outside": "^0.0.3",
                "tiny-markdown-editor": "^0.1.31",
index c91ff37d2a64974095046d2510ae010b817c0719..c25dd0025f6de89c358b9d23b77c98597d273a21 100644 (file)
@@ -2,9 +2,23 @@
        import Fa from 'svelte-fa';
        import { faTrash, faPencil, faXmark } from '@fortawesome/free-solid-svg-icons';
        let { tag, removeTag, isEditable, isRemovable, isDeletable } = $props();
+
+       let fontColor = $state('#111');
+       $effect(() => {
+               const r = parseInt(tag.color.slice(1, 3), 16);
+               const g = parseInt(tag.color.slice(3, 5), 16);
+               const b = parseInt(tag.color.slice(5, 7), 16);
+               const brightness = r * 0.299 + g * 0.587 + b * 0.114;
+
+               if (brightness > 140) {
+                       fontColor = '#111';
+               } else {
+                       fontColor = '#eee';
+               }
+       });
 </script>
 
-<span class="badge rounded-pill" style="background-color: {tag.color};">
+<span class="badge rounded-pill" style="background-color: {tag.color}; color: {fontColor}">
        <div class="d-flex flex-row">
                <div>{tag.icon} #{tag.name}</div>
                {#if isEditable}
diff --git a/frontend/src/lib/TagModal.svelte b/frontend/src/lib/TagModal.svelte
new file mode 100644 (file)
index 0000000..f3a6bf4
--- /dev/null
@@ -0,0 +1,175 @@
+<script>
+       import * as bootstrap from 'bootstrap';
+       import Tag from './Tag.svelte';
+       import { Picker } from 'emoji-picker-element';
+       import Fa from 'svelte-fa';
+       import { faTrash } from '@fortawesome/free-solid-svg-icons';
+
+       let { editTag = $bindable(), createTag = false, saveNewTag, isSaving = false } = $props();
+
+       function open() {
+               // hide tag picker
+               document.querySelector('.tooltip').classList.remove('shown');
+
+               let modal = new bootstrap.Modal(document.getElementById('modalTag'));
+               modal.show();
+       }
+
+       function close() {
+               let modal = bootstrap.Modal.getInstance(document.getElementById('modalTag'));
+               modal.hide();
+       }
+
+       export { open, close };
+
+       let pickerShown = $state(false);
+       function togglePicker() {
+               document.querySelector('.tooltip').classList.toggle('shown');
+               pickerShown = document.querySelector('.tooltip').classList.contains('shown');
+       }
+
+       function emojiSelected(ev) {
+               editTag.icon = ev.detail.unicode;
+               togglePicker();
+       }
+</script>
+
+<div class="modal fade" id="modalTag" tabindex="-1">
+       <div class="modal-dialog">
+               <div class="modal-content">
+                       <div class="modal-header">
+                               <h5 class="modal-title">
+                                       {#if createTag}
+                                               Neues Tag erstellen
+                                       {:else}
+                                               Tag bearbeiten
+                                       {/if}
+                               </h5>
+                               <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                       </div>
+                       <div class="modal-body">
+                               <div class="row">
+                                       <div class="col-4">
+                                               <h5>Emoji</h5>
+                                       </div>
+                                       <div class="col-8">
+                                               {#if editTag.icon}
+                                                       <span>{editTag.icon}</span>
+                                                       <button class="removeBtn" type="button" onclick={(editTag.icon = '')}
+                                                               ><Fa icon={faTrash} fw /></button
+                                                       >
+                                               {:else}
+                                                       <span><em>Kein Emoji ausgewählt...</em></span>
+                                               {/if}
+                                       </div>
+                               </div>
+                               <div class="row">
+                                       <div class="col-4"></div>
+                                       <div class="col-8">
+                                               <button
+                                                       class="btn btn-outline-secondary mb-2 {pickerShown ? 'active' : ''}"
+                                                       onclick={() => togglePicker()}>😀 Emoji auswählen</button
+                                               >
+                                               <!-- <em>(freiwillig)</em> -->
+                                               <div class="tooltip" role="tooltip">
+                                                       <emoji-picker class="emojiPicker" onemoji-click={(ev) => emojiSelected(ev)}
+                                                       ></emoji-picker>
+                                               </div>
+                                       </div>
+                               </div>
+
+                               <div class="row">
+                                       <div class="col-4"><h5>Name</h5></div>
+                                       <div class="col-8">
+                                               <input
+                                                       bind:value={editTag.name}
+                                                       type="text"
+                                                       class="form-control mb-2"
+                                                       placeholder="Name"
+                                               />
+                                       </div>
+                               </div>
+
+                               <div class="row">
+                                       <div class="col-4">
+                                               <h5>Farbe</h5>
+                                       </div>
+                                       <div class="col-8">
+                                               <input
+                                                       bind:value={editTag.color}
+                                                       type="color"
+                                                       class="form-control form-control-color colorInput"
+                                               />
+                                       </div>
+                               </div>
+
+                               <hr />
+                               <div class="row">
+                                       <div class="col-4"><h5>Vorschau</h5></div>
+                                       <div class="col-8"><Tag tag={editTag} /></div>
+                               </div>
+                       </div>
+                       <div class="modal-footer">
+                               <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
+                               <button
+                                       onclick={saveNewTag}
+                                       type="button"
+                                       class="btn btn-primary"
+                                       disabled={!editTag.name || isSaving}
+                                       >Speichern
+                                       {#if isSaving}
+                                               <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
+                                       {/if}
+                               </button>
+                       </div>
+               </div>
+       </div>
+</div>
+
+<style>
+       .colorInput {
+               width: 50px;
+               padding: 3px;
+               cursor: pointer;
+       }
+
+       .tooltip:not(.shown) {
+               display: none;
+       }
+
+       .tooltip {
+               display: contents;
+               position: absolute;
+               z-index: 1000;
+       }
+
+       .emojiPicker {
+               position: absolute;
+               z-index: 1000;
+       }
+
+       .removeBtn {
+               background-color: transparent;
+               border: 1px solid #ccc;
+               border-radius: 5px;
+               color: #495057;
+               cursor: pointer;
+               font-size: 11pt;
+               margin-left: 0.3rem;
+               transition: all 0.3s ease;
+       }
+
+       .removeBtn:hover {
+               color: #dc3545;
+       }
+
+       :global(.modal.show) {
+               background-color: rgba(80, 80, 80, 0.1) !important;
+               backdrop-filter: blur(2px) saturate(150%);
+       }
+
+       .modal-content {
+               backdrop-filter: blur(8px) saturate(150%);
+               background-color: rgba(219, 219, 219, 0.45);
+       }
+</style>
index 1923fe73003e4da980f6e3722f8119889201bba0..f98b4b34650574f34e5d61b8f190224c69b020eb 100644 (file)
                                        </div>
                                </div>
                                <div class="modal-footer">
-                                       <button type="button" class="btn btn-primary">Speichern</button>
                                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
+                                       <button type="button" class="btn btn-primary">Speichern</button>
                                </div>
                        </div>
                </div>
index caba1b70b931e96808ac08458954a8995fd3613b..4f02b455f59e09d1af76711b0047e230de1388e2 100644 (file)
        import '../../../node_modules/tiny-markdown-editor/dist/tiny-mde.css';
        import { API_URL } from '$lib/APIurl.js';
        import DatepickerLogic from '$lib/DatepickerLogic.svelte';
-       import { faCloudArrowUp, faCloudArrowDown, faTrash } from '@fortawesome/free-solid-svg-icons';
+       import {
+               faCloudArrowUp,
+               faCloudArrowDown,
+               faTrash,
+               faSquarePlus,
+               faQuestionCircle
+       } from '@fortawesome/free-solid-svg-icons';
        import Fa from 'svelte-fa';
        import { v4 as uuidv4 } from 'uuid';
        import { slide, fade } from 'svelte/transition';
        import { autoLoadImages } from '$lib/settingsStore';
        import Tag from '$lib/Tag.svelte';
+       import TagModal from '$lib/TagModal.svelte';
 
        axios.interceptors.request.use((config) => {
                config.withCredentials = true;
                loadTags();
 
                getLog();
+
+               // enable popovers
+               const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
+               const popoverList = [...popoverTriggerList].map(
+                       (popoverTriggerEl) =>
+                               new bootstrap.Popover(popoverTriggerEl, { trigger: 'focus', html: true })
+               );
        });
 
        let tags = $state([]);
        const imageExtensions = ['jpeg', 'jpg', 'gif', 'png', 'webp'];
        //TODO: support svg? -> minsize is necessary...
 
-       function base64ToArrayBuffer(base64) {
-               var binaryString = atob(base64);
-               var bytes = new Uint8Array(binaryString.length);
-               for (var i = 0; i < binaryString.length; i++) {
-                       bytes[i] = binaryString.charCodeAt(i);
-               }
-               return bytes.buffer;
-       }
-
        $effect(() => {
                if (filesOfDay) {
                        // add all files to images if correct extension
        function removeTag(id) {
                selectedTags = selectedTags.filter((tag) => tag.id !== id);
        }
+
+       let editTag = $state({});
+       let tagModal;
+
+       function openTagModal(tag) {
+               if (tag === null) {
+                       editTag = {
+                               icon: '',
+                               name: '',
+                               color: '#f57c00'
+                       };
+               } else {
+                       editTag = tag;
+               }
+
+               tagModal.open();
+       }
+
+       let isSavingNewTag = $state(false);
+       function saveNewTag() {
+               isSavingNewTag = true;
+               axios
+                       .post(API_URL + '/logs/saveTag', {
+                               icon: editTag.icon,
+                               name: editTag.name,
+                               color: editTag.color
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       loadTags();
+                                       tagModal.close();
+                               } else {
+                                       // toast
+                                       const toast = new bootstrap.Toast(document.getElementById('toastErrorSavingTag'));
+                                       toast.show();
+                               }
+                       })
+                       .finally(() => {
+                               // close modal
+
+                               isSavingNewTag = false;
+                       });
+       }
 </script>
 
 <DatepickerLogic />
 
        <div id="right" class="d-flex flex-column">
                <div class="tags">
-                       <h3>Tags</h3>
-                       <input
-                               bind:value={searchTab}
-                               onfocus={() => {
-                                       showTagDropdown = true;
-                                       selectedTagIndex = 0;
-                               }}
-                               onfocusout={() => {
-                                       setTimeout(() => (showTagDropdown = false), 150);
-                               }}
-                               onkeydown={handleKeyDown}
-                               type="text"
-                               class="form-control"
-                               id="tag-input"
-                               placeholder="Tag..."
-                       />
+                       <div class="d-flex flex-row justify-content-between">
+                               <h3>Tags</h3>
+                               <!-- svelte-ignore a11y_missing_attribute -->
+                               <a
+                                       tabindex="-1"
+                                       type="button"
+                                       class="btn"
+                                       data-bs-toggle="popover"
+                                       data-bs-title="Tags"
+                                       data-bs-content="Hier kannst du Tags zum ausgewählten Datum hinzufügen und entfernen, um deine Einträge zu kategorisieren. Ebenso kannst du hier neue Tags erstellen.<br/><br/>Um ein Tag zu ändern oder auch vollständig zu löschen, musst du in die Einstellungen wechseln."
+                               >
+                                       <Fa icon={faQuestionCircle} fw /></a
+                               >
+                       </div>
+                       <div class="d-flex flex-row">
+                               <input
+                                       bind:value={searchTab}
+                                       onfocus={() => {
+                                               showTagDropdown = true;
+                                               selectedTagIndex = 0;
+                                       }}
+                                       onfocusout={() => {
+                                               setTimeout(() => (showTagDropdown = false), 150);
+                                       }}
+                                       onkeydown={handleKeyDown}
+                                       type="text"
+                                       class="form-control"
+                                       id="tag-input"
+                                       placeholder="Tag..."
+                               />
+                               <button
+                                       class="btn btn-outline-secondary ms-2"
+                                       onclick={() => {
+                                               openTagModal(null);
+                                       }}
+                               >
+                                       <Fa icon={faSquarePlus} fw /> Neu
+                               </button>
+                       </div>
                        {#if showTagDropdown}
                                <div id="tagDropdown">
                                        {#if filteredTags.length === 0}
-                                               <em style="padding: 0.2rem;">Keinen Tag gefunden...</em>
+                                               <em style="padding: 0.2rem;">Kein Tag gefunden...</em>
                                        {:else}
                                                {#each filteredTags as tag, index (tag.id)}
                                                        <!-- svelte-ignore a11y_click_events_have_key_events -->
        </div>
 
        <div class="toast-container position-fixed bottom-0 end-0 p-3">
+               <div
+                       id="toastErrorSavingTag"
+                       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 des Tags!</div>
+                       </div>
+               </div>
+
                <div
                        id="toastErrorSavingLog"
                        class="toast align-items-center text-bg-danger"
                </div>
        </div>
 
+       <TagModal
+               bind:this={tagModal}
+               bind:editTag
+               createTag="true"
+               isSaving={isSavingNewTag}
+               {saveNewTag}
+       />
+
        <div
                class="modal fade"
                id="modalImages"
                gap: 0.5rem;
        }
 
+       #tag-input {
+               width: inherit !important;
+       }
+
        #tagDropdown {
                position: absolute;
                background-color: white;
git clone https://git.99rst.org/PROJECT