-import base64
import datetime
-import io
import logging
import re
-from fastapi import APIRouter, Cookie, Depends, Form, UploadFile, File, HTTPException
+from fastapi import APIRouter, Depends, Form, UploadFile, File, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from . import users
from ..utils import security
import html
from typing import Annotated
-import time
logger = logging.getLogger("dailytxtLogger")
month = datetime.datetime.fromisoformat(log.date).month
day = datetime.datetime.fromisoformat(log.date).day
- content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
# move old log to history
if "days" in content.keys():
month = datetime.datetime.fromisoformat(date).month
day = datetime.datetime.fromisoformat(date).day
- content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
dummy = {"text": "", "date_written": "", "files": [], "tags": []}
if "files" in dayLog.keys():
for file in dayLog["files"]:
file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
- file["type"] = security.decrypt_text(file["enc_type"], enc_key)
return {"text": text, "date_written": date_written, "files": dayLog.get("files", []), "tags": dayLog.get("tags", [])}
return dummy
return endIndex
+def get_begin(text):
+ # get first 5 words
+ words = text.split()
+ if len(words) < 5:
+ return text
+ return " ".join(words[:5])
+
def get_context(text: str, searchString: str, exact: bool):
# replace whitespace with non-breaking space
text = re.sub(r'\s+', " ", text)
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)):
+@router.get("/searchString")
+async def searchString(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))
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, int(month))
if "days" not in content.keys():
continue
for dayLog in content["days"]:
+ if "text" not in dayLog.keys():
+ if "files" in dayLog.keys():
+ for file in dayLog["files"]:
+ filename = security.decrypt_text(file["enc_filename"], enc_key)
+ if searchString.lower() in filename.lower():
+ context = "📎 " + filename
+ results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+ break
+ continue
+
text = security.decrypt_text(dayLog["text"], enc_key)
# "..." -> exact
if searchString.lower() in text.lower():
context = get_context(text, searchString, False)
results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+
+ elif "files" in dayLog.keys():
+ for file in dayLog["files"]:
+ filename = security.decrypt_text(file["enc_filename"], enc_key)
+ if searchString.lower() in filename.lower():
+ context = "📎 " + filename
+ results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+ break
# sort by year and month and day
results.sort(key=lambda x: (int(x["year"]), int(x["month"]), int(x["day"])), reverse=False)
return results
+@router.get("/searchTag")
+async def searchTag(tag_id: int, 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.getMonth(cookie["user_id"], year, int(month))
+ if "days" not in content.keys():
+ continue
+ for dayLog in content["days"]:
+ if "tags" not in dayLog.keys():
+ continue
+ if tag_id in dayLog["tags"]:
+ context = ''
+ if "text" in dayLog.keys():
+ text = security.decrypt_text(dayLog["text"], enc_key)
+ context = get_begin(text)
+ 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=False)
+ return results
+
@router.get("/getMarkedDays")
async def getMarkedDays(month: str, year: str, cookie = Depends(users.isLoggedIn)):
days_with_logs = []
days_with_files = []
- content:dict = fileHandling.getDay(cookie["user_id"], year, int(month))
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, int(month))
if "days" not in content.keys():
return {"days_with_logs": [], "days_with_files": []}
@router.get("/loadMonthForReading")
async def loadMonthForReading(month: int, year: int, cookie = Depends(users.isLoggedIn)):
- content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
if "days" not in content.keys():
return []
return {"success": False}
# save file in log
- content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
enc_filename = security.encrypt_text(file.filename, enc_key)
new_file = {"enc_filename": enc_filename, "uuid_filename": uuid, "size": file.size}
return {"success": True}
-"""
-@router.get("/getFiles")
-async def getFiles(day: int, month: int, year: int, cookie = Depends(users.isLoggedIn)):
- content:dict = fileHandling.getDay(cookie["user_id"], year, month)
- if "days" not in content.keys():
- return []
-
- enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
- for dayLog in content["days"]:
- if "day" in dayLog.keys() and dayLog["day"] == day:
- if "files" in dayLog.keys():
- for file in dayLog["files"]:
- file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
- file["type"] = security.decrypt_text(file["enc_type"], enc_key)
-
- return dayLog["files"]
-
- return []
-"""
@router.get("/deleteFile")
async def deleteFile(uuid: str, day: int, month: int, year: int, cookie = Depends(users.isLoggedIn)):
- content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
if "days" not in content.keys():
raise HTTPException(status_code=500, detail="Day not found - json error")
@router.post("/addTagToLog")
async def addTagToLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)):
- content:dict = fileHandling.getDay(cookie["user_id"], data.year, data.month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], data.year, data.month)
if "days" not in content.keys():
content["days"] = []
@router.post("/removeTagFromLog")
async def removeTagFromLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)):
- content:dict = fileHandling.getDay(cookie["user_id"], data.year, data.month)
+ content:dict = fileHandling.getMonth(cookie["user_id"], data.year, data.month)
if "days" not in content.keys():
raise HTTPException(status_code=500, detail="Day not found - json error")
return {}
return json.loads(s)
-def getDay(user_id, year, month):
+def getMonth(user_id, year, month):
try:
f = open(os.path.join(settings.data_path, f"{user_id}/{year}/{month:02d}.json"), "r")
except FileNotFoundError:
<script>
import Datepicker from './Datepicker.svelte';
- import { searchString, searchResults } from '$lib/searchStore.js';
+ import { searchString, searchTag, searchResults, isSearching } from '$lib/searchStore.js';
import { selectedDate } from '$lib/calendarStore.js';
+ import { tags } from '$lib/tagStore.js';
+ import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
+ import { Fa } from 'svelte-fa';
+ import { onMount } from 'svelte';
+ import * as bootstrap from 'bootstrap';
+ import Tag from './Tag.svelte';
- export let search = () => {};
+ let { searchForString, searchForTag } = $props();
+
+ onMount(() => {
+ const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
+ const popoverList = [...popoverTriggerList].map(
+ (popoverTriggerEl) =>
+ new bootstrap.Popover(popoverTriggerEl, { html: true, trigger: 'focus' })
+ );
+ });
let searchInput;
let ctrlPressed = false;
}
if (event.key === 'f' && ctrlPressed) {
event.preventDefault();
- searchInput.focus();
+ $searchTag = {};
+ setTimeout(() => {
+ searchInput.focus();
+ }, 100);
}
}
ctrlPressed = false;
}
}
+
+ let showTagDropdown = $state(false);
+ let filteredTags = $state([]);
+ let selectedTagIndex = $state(0);
+
+ function handleKeyDown(event) {
+ if (!showTagDropdown && event.key === 'Enter') searchForString();
+ if (filteredTags.length === 0) return;
+
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault(); // Prevent cursor movement
+ selectedTagIndex = Math.min(selectedTagIndex + 1, filteredTags.length - 1);
+ ensureSelectedVisible();
+ 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();
+ selectSearchTag(filteredTags[selectedTagIndex].id);
+ }
+ document.activeElement.blur();
+ break;
+
+ case 'Escape':
+ showTagDropdown = false;
+ break;
+ }
+ }
+
+ function ensureSelectedVisible() {
+ setTimeout(() => {
+ for (let i = 0; i < 2; i++) {
+ const dropdown = document.querySelectorAll('.searchTagDropdown')[i];
+ const selectedElement = dropdown?.querySelector('.searchTag-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;
+ }
+ }
+ }
+ }, 40);
+ }
+
+ $effect(() => {
+ let search = $searchString;
+ if (search.startsWith('#')) {
+ search = search.slice(1);
+ showTagDropdown = true;
+ filteredTags = $tags.filter((tag) => tag.name.toLowerCase().includes(search.toLowerCase()));
+ } else {
+ showTagDropdown = false;
+ }
+ });
+
+ //let searchTag = $state({});
+ function selectSearchTag(tagId) {
+ const tag = $tags.find((tag) => tag.id === tagId);
+ if (!tag) {
+ return;
+ }
+ $searchTag = tag;
+ //$searchResults = [];
+ showTagDropdown = false;
+
+ searchForTag();
+ }
+
+ function removeSearchTag() {
+ $searchTag = {};
+ $searchResults = [];
+ }
</script>
<svelte:window onkeydown={on_key_down} onkeyup={on_key_up} />
<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}
+ <div class="search">
+ <form onsubmit={searchForString} class="input-group mt-5">
<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'
- : ''}"
+ class="btn btn-outline-secondary"
+ data-bs-toggle="popover"
+ data-bs-title="Suche"
+ data-bs-content="Hier kannst "
+ onclick={(event) => event.preventDefault()}><Fa icon={faQuestionCircle} /></button
>
- <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'
- })}
+ {#if $searchTag.id}
+ <!-- If a tag is selected ... -->
+ <div class="ms-1 align-content-center">
+ <Tag tag={$searchTag} removeTag={removeSearchTag} isRemovable />
+ </div>
+ {#if $isSearching}
+ <div class="ms-2 align-content-center">
+ <div class="spinner-border spinner-border-sm" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
</div>
- <!-- <div class="search-separator"></div> -->
- <div class="text">
- {@html result.text}
+ {/if}
+ {:else}
+ <input
+ bind:value={$searchString}
+ bind:this={searchInput}
+ id="search-input"
+ type="text"
+ class="form-control"
+ placeholder="Suche"
+ aria-label="Suche"
+ aria-describedby="search-button"
+ onkeydown={handleKeyDown}
+ autocomplete="off"
+ onfocus={() => {
+ selectedTagIndex = 0;
+ if ($searchString.startsWith('#')) {
+ showTagDropdown = true;
+ }
+ }}
+ onfocusout={() => {
+ setTimeout(() => (showTagDropdown = false), 150);
+ }}
+ />
+ <button class="btn btn-outline-secondary" type="submit" id="search-button">
+ {#if $isSearching}
+ <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
+ {:else}
+ Suche
+ {/if}
+ </button>
+ {/if}
+ </form>
+ {#if showTagDropdown}
+ <div class="searchTagDropdown">
+ {#if filteredTags.length === 0}
+ <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 -->
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <!-- svelte-ignore a11y_mouse_events_have_key_events -->
+ <div
+ role="button"
+ tabindex="0"
+ onclick={() => selectSearchTag(tag.id)}
+ onmouseover={() => (selectedTagIndex = index)}
+ class="searchTag-item {index === selectedTagIndex ? 'selected' : ''}"
+ >
+ <Tag {tag} />
+ </div>
+ {/each}
+ {/if}
+ </div>
+ {/if}
+ <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>
- </div>
- </button>
- {/each}
+ </button>
+ {/each}
+ </div>
</div>
</div>
<style>
+ .searchTagDropdown {
+ position: absolute;
+ background-color: white;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ left: 60px;
+ max-height: 150px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .searchTag-item.selected {
+ background-color: #b2b4b6;
+ }
+
+ .searchTag-item {
+ cursor: pointer;
+ padding: 5px;
+ }
+
.search-result-content {
display: flex;
align-items: center;
import { writable } from "svelte/store";
export let searchString = writable("");
-export let searchResults = writable([]);
\ No newline at end of file
+export let searchResults = writable([]);
+export let searchTag = writable({});
+export let isSearching = writable(false);
\ No newline at end of file
--- /dev/null
+import {writable} from 'svelte/store';
+
+export let tags = writable([]);
\ No newline at end of file
import axios from 'axios';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
- import { searchString, searchResults } from '$lib/searchStore.js';
+ import { searchString, searchTag, searchResults, isSearching } from '$lib/searchStore.js';
import * as TinyMDE from 'tiny-markdown-editor';
import '../../../node_modules/tiny-markdown-editor/dist/tiny-mde.css';
import { API_URL } from '$lib/APIurl.js';
import { v4 as uuidv4 } from 'uuid';
import { slide, fade } from 'svelte/transition';
import { autoLoadImages } from '$lib/settingsStore';
+ import { tags } from '$lib/tagStore';
import Tag from '$lib/Tag.svelte';
import TagModal from '$lib/TagModal.svelte';
);
});
- let tags = $state([]);
function loadTags() {
axios
.get(API_URL + '/logs/getTags')
.then((response) => {
- tags = response.data;
+ $tags = response.data;
})
.catch((error) => {
console.error(error);
event.preventDefault();
altPressed = false;
}
+ if (event.key === 'Control') {
+ event.preventDefault();
+ ctrlPressed = false;
+ }
}
function changeDay(increment) {
}
});
- let isSearching = $state(false);
- function search() {
- console.log($searchString);
-
- if (isSearching) {
+ function searchForString() {
+ if ($isSearching) {
return;
}
- isSearching = true;
+ $isSearching = true;
axios
- .get(API_URL + '/logs/search', {
+ .get(API_URL + '/logs/searchString', {
params: {
searchString: $searchString
}
})
.then((response) => {
$searchResults = [...response.data];
- isSearching = false;
+ $isSearching = false;
})
.catch((error) => {
$searchResults = [];
console.error(error);
- isSearching = false;
+ $isSearching = false;
+
+ // toast
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorSearching'));
+ toast.show();
+ });
+ }
+
+ function searchForTag() {
+ $searchString = '';
+ if ($isSearching) {
+ return;
+ }
+ $isSearching = true;
+ axios
+ .get(API_URL + '/logs/searchTag', { params: { tag_id: $searchTag.id } })
+ .then((response) => {
+ $searchResults = [...response.data];
+ $isSearching = false;
+ })
+ .catch((error) => {
+ $isSearching = false;
+ $searchResults = [];
+
+ console.error(error);
// toast
const toast = new bootstrap.Toast(document.getElementById('toastErrorSearching'));
toast.show();
// show the correct tags in the dropdown
$effect(() => {
- if (tags.length === 0) {
+ if ($tags.length === 0) {
filteredTags = [];
return;
}
// exclude already selected tags
- let tagsWithoutSelected = tags.filter(
+ let tagsWithoutSelected = $tags.filter(
(tag) => !selectedTags.find((selectedTag) => selectedTag === tag.id)
);
event.preventDefault(); // Prevent cursor movement
selectedTagIndex = Math.min(selectedTagIndex + 1, filteredTags.length - 1);
ensureSelectedVisible();
- console.log(selectedTagIndex);
break;
case 'ArrowUp':
dropdown.scrollTop += selectedRect.bottom - dropdownRect.bottom;
}
}
- }, 0);
+ }, 40);
}
let showTagLoading = $state(false);
aria-label="Close"
></button>
</div>
- <Sidenav {search} />
+ <Sidenav {searchForString} {searchForTag} />
</div>
<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 {search} />
+ <Sidenav {searchForString} {searchForTag} />
</div>
<!-- Center -->
<Fa icon={faQuestionCircle} fw /></a
>
</div>
- <div class="d-flex flex-row">
+ <div class="tagRow d-flex flex-row">
<input
bind:value={searchTab}
onfocus={() => {
placeholder="Tag..."
/>
<button
- class="btn btn-outline-secondary ms-2"
+ class="newTagBtn btn btn-outline-secondary ms-2"
onclick={() => {
openTagModal(null);
}}
</div>
{/if}
<div class="selectedTags d-flex flex-row flex-wrap">
- {#if tags.length !== 0}
+ {#if $tags.length !== 0}
{#each selectedTags as tag_id (tag_id)}
<div transition:slide={{ axis: 'x' }}>
- <Tag tag={tags.find((tag) => tag.id === tag_id)} {removeTag} isRemovable="true" />
+ <Tag tag={$tags.find((tag) => tag.id === tag_id)} {removeTag} isRemovable="true" />
</div>
{/each}
{/if}
</div>
<style>
+ .tagRow {
+ width: 100%;
+ }
+
+ .newTagBtn {
+ white-space: nowrap;
+ }
+
.tag-item.selected {
background-color: #b2b4b6;
}