import datetime
import logging
+import re
from fastapi import APIRouter, Cookie
from pydantic import BaseModel
from fastapi import Depends
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
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
--- /dev/null
+import { writable } from "svelte/store";
+
+export let searchString = writable("");
+export let searchResults = writable([]);
\ No newline at end of file
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;
font-family: Arial, sans-serif;
border: 1px solid #ccc;
border-radius: 8px;
- overflow: hidden;
+ /* overflow: hidden; */
width: 300px;
box-sizing: border-box;
}
<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>