huge initial progress with tags
authorPhiTux <redacted>
Wed, 5 Mar 2025 23:39:02 +0000 (00:39 +0100)
committerPhiTux <redacted>
Wed, 5 Mar 2025 23:39:02 +0000 (00:39 +0100)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/src/lib/Tag.svelte [new file with mode: 0644]
frontend/src/routes/write/+page.svelte

index f7b79c1ed1816903d5da41421243864c28435bb2..839ea77cefec44bcdab6800a2eb8ab0eb104ca16 100644 (file)
@@ -327,4 +327,16 @@ async def downloadFile(uuid: str, cookie = Depends(users.isLoggedIn)):
     if file is None:
         raise HTTPException(status_code=500, detail="Failed to read file")
     content = security.decrypt_file(file, enc_key)
-    return StreamingResponse(iter([content]))
\ No newline at end of file
+    return StreamingResponse(iter([content]))
+
+@router.get("/getTags")
+async def getTags(cookie = Depends(users.isLoggedIn)):
+    content:dict = fileHandling.getTags(cookie["user_id"])
+
+    if not 'tags' in content:
+        return []
+    
+    else:
+        return content['tags']
+
+    ### NOCH ENTSCHLÜSSELN!!!!
\ No newline at end of file
index 82d506cc6087dda6b097e15a6ff3cee066fa7e0d..f2ccef88c6cf16fc8145cf66cc44d1608b4af9ae 100644 (file)
@@ -105,4 +105,20 @@ def removeFile(user_id, uuid):
         logger.exception(e)
         return False
     else:
-        return True
\ No newline at end of file
+        return True
+    
+def getTags(user_id):
+    try:
+        f = open(os.path.join(settings.data_path, str(user_id), "tags.json"), "r")
+    except FileNotFoundError:
+        logger.info(f"{user_id}/tags.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 tags.json")
+    else:
+        with f:
+            s = f.read()
+            if s == "":
+                return {}
+            return json.loads(s)
\ No newline at end of file
diff --git a/frontend/src/lib/Tag.svelte b/frontend/src/lib/Tag.svelte
new file mode 100644 (file)
index 0000000..c91ff37
--- /dev/null
@@ -0,0 +1,53 @@
+<script>
+       import Fa from 'svelte-fa';
+       import { faTrash, faPencil, faXmark } from '@fortawesome/free-solid-svg-icons';
+       let { tag, removeTag, isEditable, isRemovable, isDeletable } = $props();
+</script>
+
+<span class="badge rounded-pill" style="background-color: {tag.color};">
+       <div class="d-flex flex-row">
+               <div>{tag.icon} #{tag.name}</div>
+               {#if isEditable}
+                       <button class="button btnEdit">
+                               <Fa icon={faPencil} fw />
+                       </button>
+               {/if}
+               {#if isRemovable}
+                       <button onclick={() => removeTag(tag.id)} class="button btnRemove">
+                               <Fa icon={faXmark} fw />
+                       </button>
+               {/if}
+               {#if isDeletable}
+                       <button class="button btnRemove">
+                               <Fa icon={faTrash} fw />
+                       </button>
+               {/if}
+       </div>
+</span>
+
+<style>
+       span {
+               background-color: #f8f9fa;
+               color: #495057;
+               font-size: 11pt;
+               font-weight: 600;
+       }
+
+       button {
+               background-color: transparent;
+               border: none;
+               color: #495057;
+               cursor: pointer;
+               font-size: 11pt;
+               margin-left: 0.3rem;
+               transition: all 0.3s ease;
+       }
+
+       .btnRemove:hover {
+               color: #dc3545;
+       }
+
+       .btnEdit:hover {
+               color: #007bff;
+       }
+</style>
index 01ab16d43151056d5014ce812657f9d66603b1c6..caba1b70b931e96808ac08458954a8995fd3613b 100644 (file)
@@ -16,6 +16,7 @@
        import { v4 as uuidv4 } from 'uuid';
        import { slide, fade } from 'svelte/transition';
        import { autoLoadImages } from '$lib/settingsStore';
+       import Tag from '$lib/Tag.svelte';
 
        axios.interceptors.request.use((config) => {
                config.withCredentials = true;
                        handleInput();
                });
 
+               loadTags();
+
                getLog();
        });
 
+       let tags = $state([]);
+       function loadTags() {
+               axios
+                       .get(API_URL + '/logs/getTags')
+                       .then((response) => {
+                               tags = response.data;
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               // toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorLoadingTags'));
+                               toast.show();
+                       });
+       }
+
        $effect(() => {
                if (currentLog !== savedLog) {
                        document.getElementsByClassName('TinyMDE')[0].classList.add('notSaved');
        });
 
        let altPressed = false;
+       let ctrlPressed = false;
        function on_key_down(event) {
                if (event.key === 'Alt') {
                        event.preventDefault();
                        event.preventDefault();
                        changeDay(-1);
                }
+               if (event.key === 'Control') {
+                       event.preventDefault();
+                       ctrlPressed = true;
+               }
+               if (event.key === 'g' && ctrlPressed) {
+                       event.preventDefault();
+                       document.getElementById('tag-input').focus();
+               }
        }
 
        function on_key_up(event) {
                const modal = new bootstrap.Modal(document.getElementById('modalImages'));
                modal.show();
        }
+
+       let searchTab = $state('');
+       let showTagDropdown = $state(false);
+
+       let filteredTags = $state([]);
+       let selectedTags = $state([]);
+
+       // show the correct tags in the dropdown
+       $effect(() => {
+               // exclude already selected tags
+               let tagsWithoutSelected = tags.filter(
+                       (tag) => !selectedTags.find((selectedTag) => selectedTag.id === tag.id)
+               );
+
+               if (searchTab === '') {
+                       filteredTags = tagsWithoutSelected;
+               } else {
+                       // remove trailing # if present
+                       let searchString = searchTab;
+                       if (searchString.startsWith('#')) {
+                               searchString = searchString.slice(1);
+                       }
+
+                       // filter tags for searchstring
+                       filteredTags = tagsWithoutSelected.filter((tag) =>
+                               tag.name.toLowerCase().includes(searchString.toLowerCase())
+                       );
+               }
+
+               selectedTagIndex = 0;
+       });
+
+       let selectedTagIndex = $state(0);
+       // Handle Keyboard Navigation in Tag Dropdown
+       function handleKeyDown(event) {
+               if (!showTagDropdown || filteredTags.length === 0) return;
+
+               switch (event.key) {
+                       case 'ArrowDown':
+                               event.preventDefault(); // Prevent cursor movement
+                               selectedTagIndex = Math.min(selectedTagIndex + 1, filteredTags.length - 1);
+                               ensureSelectedVisible();
+                               console.log(selectedTagIndex);
+                               break;
+
+                       case 'ArrowUp':
+                               event.preventDefault(); // Prevent cursor movement
+                               selectedTagIndex = Math.max(selectedTagIndex - 1, 0);
+                               ensureSelectedVisible();
+                               break;
+
+                       case 'Enter':
+                               if (selectedTagIndex >= 0 && selectedTagIndex < filteredTags.length) {
+                                       event.preventDefault();
+                                       selectTag(filteredTags[selectedTagIndex].id);
+                               }
+                               document.activeElement.blur();
+                               break;
+
+                       case 'Escape':
+                               showTagDropdown = false;
+                               break;
+               }
+       }
+
+       function ensureSelectedVisible() {
+               setTimeout(() => {
+                       const dropdown = document.getElementById('tagDropdown');
+                       const selectedElement = dropdown?.querySelector('.tag-item.selected');
+
+                       if (dropdown && selectedElement) {
+                               const dropdownRect = dropdown.getBoundingClientRect();
+                               const selectedRect = selectedElement.getBoundingClientRect();
+
+                               if (selectedRect.top < dropdownRect.top) {
+                                       dropdown.scrollTop -= dropdownRect.top - selectedRect.top;
+                               } else if (selectedRect.bottom > dropdownRect.bottom) {
+                                       dropdown.scrollTop += selectedRect.bottom - dropdownRect.bottom;
+                               }
+                       }
+               }, 0);
+       }
+
+       function selectTag(id) {
+               selectedTags = [...selectedTags, tags.find((tag) => tag.id === id)];
+               searchTab = '';
+       }
+
+       function removeTag(id) {
+               selectedTags = selectedTags.filter((tag) => tag.id !== id);
+       }
 </script>
 
 <DatepickerLogic />
        </div>
 
        <div id="right" class="d-flex flex-column">
-               <div>Tags</div>
+               <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..."
+                       />
+                       {#if showTagDropdown}
+                               <div id="tagDropdown">
+                                       {#if filteredTags.length === 0}
+                                               <em style="padding: 0.2rem;">Keinen Tag gefunden...</em>
+                                       {:else}
+                                               {#each filteredTags as tag, index (tag.id)}
+                                                       <!-- svelte-ignore a11y_click_events_have_key_events -->
+                                                       <!-- svelte-ignore a11y_no_static_element_interactions -->
+                                                       <!-- svelte-ignore a11y_mouse_events_have_key_events -->
+                                                       <div
+                                                               role="button"
+                                                               tabindex="0"
+                                                               onclick={() => selectTag(tag.id)}
+                                                               onmouseover={() => (selectedTagIndex = index)}
+                                                               class="tag-item {index === selectedTagIndex ? 'selected' : ''}"
+                                                       >
+                                                               <Tag {tag} />
+                                                       </div>
+                                               {/each}
+                                       {/if}
+                               </div>
+                       {/if}
+                       <div class="selectedTags d-flex flex-row flex-wrap">
+                               {#each selectedTags as tag (tag.id)}
+                                       <Tag {tag} {removeTag} isRemovable="true" />
+                               {/each}
+                       </div>
+               </div>
 
                <div class="files d-flex flex-column">
                        <button
                                        <button
                                                class="p-2 fileBtn deleteFileBtn"
                                                onclick={() => askDeleteFile(file.uuid_filename, file.filename)}
-                                               ><Fa icon={faTrash} id="uploadIcon" fw /></button
+                                               ><Fa icon={faTrash} fw /></button
                                        >
                                </div>
                        {/each}
 </div>
 
 <style>
+       .tag-item.selected {
+               background-color: #b2b4b6;
+       }
+
+       .selectedTags {
+               margin-top: 0.5rem;
+               gap: 0.5rem;
+       }
+
+       #tagDropdown {
+               position: absolute;
+               background-color: white;
+               box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+               z-index: 1000;
+               max-height: 150px;
+               overflow-y: scroll;
+               overflow-x: hidden;
+               display: flex;
+               flex-direction: column;
+       }
+
+       .tag-item {
+               cursor: pointer;
+               padding: 5px;
+       }
+
+       .tags {
+               z-index: 10;
+               padding: 0.5rem;
+               margin-right: 2rem;
+               margin-bottom: 2rem;
+               backdrop-filter: blur(8px) saturate(150%);
+               background-color: rgba(219, 219, 219, 0.45);
+               border: 1px solid #ececec77;
+               border-radius: 10px;
+       }
+
        #loadImageBtn {
                padding: 0.5rem 1rem;
                border: none;
        }
 
        #right {
+               margin-top: 1.5rem !important;
                min-width: 300px;
                max-width: 400px;
        }
git clone https://git.99rst.org/PROJECT