added search function (including primitive exact/OR/AND)
authorPhiTux <redacted>
Fri, 3 Jan 2025 17:16:46 +0000 (18:16 +0100)
committerPhiTux <redacted>
Fri, 3 Jan 2025 17:16:46 +0000 (18:16 +0100)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/src/lib/searchStore.js [new file with mode: 0644]
frontend/src/routes/+page.svelte
frontend/src/routes/Datepicker.svelte
frontend/src/routes/Sidenav.svelte

index a6cf338480be17a9ae0a277934cea424476f3465..1d7a7859f45199151a41153efbcc805485c09ecc 100644 (file)
@@ -1,5 +1,6 @@
 import datetime
 import logging
+import re
 from fastapi import APIRouter, Cookie
 from pydantic import BaseModel
 from fastapi import Depends
@@ -86,4 +87,100 @@ async def getLog(date: str, cookie = Depends(users.isLoggedIn)):
             date_written = security.decrypt_text(dayLog["date_written"], enc_key)
             return {"text": text, "date_written": date_written}
 
-    return {"text": "", "date_written": ""}
\ No newline at end of file
+    return {"text": "", "date_written": ""}
+
+def get_start_index(text, index):
+    # find a whitespace two places before the index
+    
+    if index == 0:
+        return 0
+    
+    for i in range(3):
+        startIndex = text.rfind(" ", 0, index-1)
+        index = startIndex
+        if startIndex == -1:
+            return 0
+
+    return startIndex + 1
+
+def get_end_index(text, index):
+    # find a whitespace two places after the index
+    
+    if index == len(text) - 1:
+        return len(text)
+    
+    for i in range(3):
+        endIndex = text.find(" ", index+1)
+        index = endIndex
+        if endIndex == -1:
+            return len(text)
+
+    return endIndex
+
+
+def get_context(text: str, searchString: str, exact: bool):
+    # replace whitespace with non-breaking space
+    text = re.sub(r'\s+', " ", text)
+
+    if exact:
+        pos = text.find(searchString)
+    else:
+        pos = text.lower().find(searchString.lower())
+    if pos == -1:
+        return "<em>Dailytxt: Error formatting...</em>"
+
+    start = get_start_index(text, pos)
+    end = get_end_index(text, pos + len(searchString) - 1)
+    return text[start:pos] + "<b>" + text[pos:pos+len(searchString)] + "</b>" + text[pos+len(searchString):end]
+
+
+@router.get("/search")
+async def search(searchString: str, cookie = Depends(users.isLoggedIn)):
+    results = []
+    
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+    
+    # search in all years and months (dirs)
+    for year in fileHandling.get_years(cookie["user_id"]):
+        for month in fileHandling.get_months(cookie["user_id"], year):
+            content:dict = fileHandling.getDay(cookie["user_id"], year, int(month))
+            if "days" not in content.keys():
+                continue
+            for dayLog in content["days"]:
+                text = security.decrypt_text(dayLog["text"], enc_key)
+                
+                # "..." -> exact
+                # ... | ... -> or
+                # ...  ... -> and
+
+                if searchString.startswith('"') and searchString.endswith('"'):
+                    if searchString[1:-1] in text:
+                        context = get_context(text, searchString[1:-1], True)
+                        results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                        
+                
+                elif "|" in searchString:
+                    words = searchString.split("|")
+                    for word in words:
+                        if word.strip().lower() in text.lower():
+                            context = get_context(text, word.strip(), False)
+                            results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                            break
+                            
+
+                elif " " in searchString:
+                    if all([word.strip().lower() in text.lower() for word in searchString.split()]):
+                        context = get_context(text, searchString.split()[0].strip(), False)
+                        results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                        
+                
+                else:
+                    if searchString.lower() in text.lower():
+                        context = get_context(text, searchString, False)
+                        results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                        
+        
+    # sort by year and month and day
+    results.sort(key=lambda x: (int(x["year"]), int(x["month"]), int(x["day"])), reverse=True)
+    print(results)
+    return results
\ No newline at end of file
index fb78d71881f8e57e1268517051043cbf87c57a2f..0bc50acedb44ce8ff1ea3c01ab8ac0ec62112c6d 100644 (file)
@@ -61,4 +61,14 @@ def writeDay(user_id, year, month, content):
     else:
         with f:
             f.write(json.dumps(content, indent=4))
-            return True
\ No newline at end of file
+            return True
+        
+def get_years(user_id):
+    for entry in os.scandir(os.path.join(settings.data_path, str(user_id))):
+        if entry.is_dir() and entry.name.isnumeric() and len(entry.name) == 4:
+            yield entry.name
+
+def get_months(user_id, year):
+    for entry in os.scandir(os.path.join(settings.data_path, str(user_id), year)):
+        if entry.is_file() and entry.name.endswith(".json"):
+            yield entry.name.split(".")[0]
\ No newline at end of file
diff --git a/frontend/src/lib/searchStore.js b/frontend/src/lib/searchStore.js
new file mode 100644 (file)
index 0000000..b21437a
--- /dev/null
@@ -0,0 +1,4 @@
+import { writable } from "svelte/store";
+
+export let searchString = writable("");
+export let searchResults = writable([]);
\ No newline at end of file
index 3ff8dc864b8935d5f6eeae51550f9bbfae612c61..ed672abaeadf4b7875e74e26c8e66ce14471a924 100644 (file)
@@ -7,7 +7,7 @@
        import { dev } from '$app/environment';
        import { goto } from '$app/navigation';
        import { onMount } from 'svelte';
-       //import { selectedDate } from './calendar.svelte.js';
+       import { searchString, searchResults } from '$lib/searchStore.js';
 
        let API_URL = dev
                ? `${window.location.origin.replace(/:5173.*$/gm, '')}:8000`
                        return false;
                }
        }
+
+       $effect(() => {
+               if ($searchString === '') {
+                       $searchResults = [];
+               }
+       });
+
+       let isSearching = $state(false);
+       function search() {
+               console.log($searchString);
+
+               if (isSearching) {
+                       return;
+               }
+               isSearching = true;
+
+               axios
+                       .get(API_URL + '/logs/search', {
+                               params: {
+                                       searchString: $searchString
+                               }
+                       })
+                       .then((response) => {
+                               $searchResults = [...response.data];
+                               isSearching = false;
+                       })
+                       .catch((error) => {
+                               $searchResults = [];
+                               console.error(error);
+                               isSearching = false;
+
+                               // toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorSearching'));
+                               toast.show();
+                       });
+       }
 </script>
 
 <svelte:window onkeydown={on_key_down} onkeyup={on_key_up} />
                        aria-label="Close"
                ></button>
        </div>
-       <Sidenav />
+       <Sidenav {search} />
 </div>
 
-<div class="d-flex flex-row justify-content-between">
+<div class="d-flex flex-row justify-content-between h-100">
        <!-- shown on large Screen -->
        <div class="d-md-block d-none sidenav p-3">
-               <Sidenav />
+               <Sidenav {search} />
        </div>
 
        <!-- Center -->
                        <div class="d-flex flex-row textAreaHeader">
                                <div class="flex-fill textAreaDate">
                                        {$selectedDate.toLocaleDateString('locale', { weekday: 'long' })}<br />
-                                       {$selectedDate.toLocaleDateString('locale')}
+                                       {$selectedDate.toLocaleDateString('locale', {
+                                               day: '2-digit',
+                                               month: '2-digit',
+                                               year: 'numeric'
+                                       })}
                                </div>
                                <div class="flex-fill textAreaWrittenAt">
                                        <div class={logDateWritten ? '' : 'opacity-50'}>Geschrieben am:</div>
-                                       <!-- <br /> -->
                                        {logDateWritten}
                                </div>
                                <div class="textAreaHistory">history</div>
                                <div class="toast-body">Fehler beim Laden des Textes!</div>
                        </div>
                </div>
+
+               <div
+                       id="toastErrorSearching"
+                       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 Suchen!</div>
+                       </div>
+               </div>
        </div>
 </div>
 
 <style>
+       .sidenav {
+               max-width: 430px;
+       }
+
        .textAreaHeader {
                border-left: 1px solid #ccc;
                border-top: 1px solid #ccc;
index 6fca948c89899894f3cc894de364f7a1458e90d9..e865f2cc328ec3d559b1fbf141b076299406bbcb 100644 (file)
                font-family: Arial, sans-serif;
                border: 1px solid #ccc;
                border-radius: 8px;
-               overflow: hidden;
+               /* overflow: hidden; */
                width: 300px;
                box-sizing: border-box;
        }
index dfc2f56bb1efcd8ac58892bead15304b1519967b..648a9a0e6abaa00975544234ff8151ed5492cf7b 100644 (file)
@@ -1,7 +1,114 @@
 <script>
        import Datepicker from './Datepicker.svelte';
+       import { searchString, searchResults } from '$lib/searchStore.js';
+       import { selectedDate } from '$lib/calendarStore.js';
+
+       export let search = () => {};
+
+       let searchInput;
+       let ctrlPressed = false;
+       function on_key_down(event) {
+               if (event.key === 'Control') {
+                       event.preventDefault();
+                       ctrlPressed = true;
+               }
+               if (event.key === 'f' && ctrlPressed) {
+                       event.preventDefault();
+                       searchInput.focus();
+               }
+       }
+
+       function on_key_up(event) {
+               if (event.key === 'Control') {
+                       event.preventDefault();
+                       ctrlPressed = false;
+               }
+       }
 </script>
 
-<Datepicker />
-<br />
-Search
+<svelte:window onkeydown={on_key_down} onkeyup={on_key_up} />
+
+<div class="d-flex flex-column h-100">
+       <Datepicker />
+       <br />
+
+       <form onsubmit={search} class="input-group mt-5">
+               <input
+                       bind:value={$searchString}
+                       bind:this={searchInput}
+                       id="search-input"
+                       type="text"
+                       class="form-control"
+                       placeholder="Suche"
+                       aria-label="Suche"
+                       aria-describedby="search-button"
+               />
+               <button class="btn btn-outline-secondary" type="submit" id="search-button">Suche</button>
+       </form>
+       <div class="list-group flex-grow-1 mb-2">
+               {#each $searchResults as result}
+                       <button
+                               type="button"
+                               onclick={() => {
+                                       $selectedDate = new Date(Date.UTC(result.year, result.month - 1, result.day));
+                               }}
+                               class="list-group-item list-group-item-action {$selectedDate.toDateString() ===
+                               new Date(Date.UTC(result.year, result.month - 1, result.day)).toDateString()
+                                       ? 'active'
+                                       : ''}"
+                       >
+                               <div class="search-result-content">
+                                       <div class="date">
+                                               {new Date(result.year, result.month - 1, result.day).toLocaleDateString('locale', {
+                                                       day: '2-digit',
+                                                       month: '2-digit',
+                                                       year: 'numeric'
+                                               })}
+                                       </div>
+                                       <!-- <div class="search-separator"></div> -->
+                                       <div class="text">
+                                               {@html result.text}
+                                       </div>
+                               </div>
+                       </button>
+               {/each}
+       </div>
+</div>
+
+<style>
+       .search-result-content {
+               display: flex;
+               align-items: center;
+       }
+
+       .date {
+               text-align: left;
+       }
+
+       .text {
+               flex-grow: 1;
+               word-wrap: break-word;
+               border-left: 1px solid #68a1da;
+               margin-left: 1rem;
+               padding-left: 1rem;
+       }
+
+       #search-input {
+               border-bottom-left-radius: 0;
+       }
+
+       #search-button {
+               border-bottom-right-radius: 0;
+       }
+
+       .list-group {
+               border-top-left-radius: 0;
+               border-top-right-radius: 0;
+               overflow-y: auto;
+               min-height: 250px;
+       }
+
+       .input-group {
+               height: auto !important;
+       }
+</style>
git clone https://git.99rst.org/PROJECT