@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
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
"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",
"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",
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}
--- /dev/null
+<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>
</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>
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;