if not found:
content["days"].append({"day": day, "text": encrypted_text, "date_written": encrypted_date_written})
- if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+ if not fileHandling.writeMonth(cookie["user_id"], year, month, content):
logger.error(f"Failed to save log for {cookie['user_id']} on {year}-{month:02d}-{day:02d}")
return {"success": False}
if not found:
content["days"].append({"day": day, "files": [new_file]})
- if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+ if not fileHandling.writeMonth(cookie["user_id"], year, month, content):
fileHandling.removeFile(cookie["user_id"], uuid)
return {"success": False}
if not fileHandling.removeFile(cookie["user_id"], uuid):
raise HTTPException(status_code=500, detail="Failed to delete file")
dayLog["files"].remove(file)
- if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+ if not fileHandling.writeMonth(cookie["user_id"], year, month, content):
raise HTTPException(status_code=500, detail="Failed to write changes of deleted file!")
return {"success": True}
return {"success": False}
else:
return {"success": True}
+
+
+class EditTag(BaseModel):
+ id: int
+ icon: str
+ name: str
+ color: str
+
+@router.post("/editTag")
+async def editTag(editTag: EditTag, 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:
+ raise HTTPException(status_code=500, detail="Tag not found - json error")
+
+ for tag in content['tags']:
+ if tag['id'] == editTag.id:
+ tag['icon'] = security.encrypt_text(editTag.icon, enc_key)
+ tag['name'] = security.encrypt_text(editTag.name, enc_key)
+ tag['color'] = security.encrypt_text(editTag.color, enc_key)
+ if not fileHandling.writeTags(cookie["user_id"], content):
+ raise HTTPException(status_code=500, detail="Failed to write tag - error writing tags")
+ else:
+ return {"success": True}
+
+ raise HTTPException(status_code=500, detail="Tag not found - not in tags")
+
+@router.get("/deleteTag")
+async def deleteTag(id: int, cookie = Depends(users.isLoggedIn)):
+ # remove from every log if present
+ 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" in dayLog.keys() and id in dayLog["tags"]:
+ dayLog["tags"].remove(id)
+ if not fileHandling.writeMonth(cookie["user_id"], year, int(month), content):
+ raise HTTPException(status_code=500, detail="Failed to delete tag - error writing log")
+
+ # remove from tags
+ content:dict = fileHandling.getTags(cookie["user_id"])
+ if not 'tags' in content:
+ raise HTTPException(status_code=500, detail="Tag not found - json error")
+
+ for tag in content['tags']:
+ if tag['id'] == id:
+ content['tags'].remove(tag)
+ if not fileHandling.writeTags(cookie["user_id"], content):
+ raise HTTPException(status_code=500, detail="Failed to delete tag - error writing tags")
+ else:
+ return {"success": True}
+ raise HTTPException(status_code=500, detail="Tag not found - not in tags")
class AddTagToLog(BaseModel):
day: int
if not dayFound:
content["days"].append({"day": data.day, "tags": [data.tag_id]})
- if not fileHandling.writeDay(cookie["user_id"], data.year, data.month, content):
+ if not fileHandling.writeMonth(cookie["user_id"], data.year, data.month, content):
raise HTTPException(status_code=500, detail="Failed to write tag - error writing log")
return {"success": True}
if not data.tag_id in dayLog["tags"]:
raise HTTPException(status_code=500, detail="Failed to remove tag - not found in log")
dayLog["tags"].remove(data.tag_id)
- if not fileHandling.writeDay(cookie["user_id"], data.year, data.month, content):
+ if not fileHandling.writeMonth(cookie["user_id"], data.year, data.month, content):
raise HTTPException(status_code=500, detail="Failed to remove tag - error writing log")
return {"success": True}
\ No newline at end of file
f.write(json.dumps(content, indent=4))
return True
-def writeDay(user_id, year, month, content):
+def writeMonth(user_id, year, month, content):
try:
os.makedirs(os.path.join(settings.data_path, f"{user_id}/{year}"), exist_ok=True)
f = open(os.path.join(settings.data_path, f"{user_id}/{year}/{month:02d}.json"), "w")
<script>
import Fa from 'svelte-fa';
import { faTrash, faPencil, faXmark } from '@fortawesome/free-solid-svg-icons';
- let { tag, removeTag, isEditable, isRemovable, isDeletable } = $props();
+ let { tag, removeTag, deleteTag, editTag, isEditable, isRemovable, isDeletable } = $props();
let fontColor = $state('#111');
$effect(() => {
<div class="d-flex flex-row">
<div>{tag.icon} #{tag.name}</div>
{#if isEditable}
- <button class="button btnEdit">
+ <button onclick={() => editTag(tag.id)} class="button btnEdit">
<Fa icon={faPencil} fw />
</button>
{/if}
</button>
{/if}
{#if isDeletable}
- <button class="button btnRemove">
+ <button onclick={() => deleteTag(tag.id)} class="button btnRemove">
<Fa icon={faTrash} fw />
</button>
{/if}
import Fa from 'svelte-fa';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
- let { editTag = $bindable(), createTag = false, saveNewTag, isSaving = false } = $props();
+ let {
+ editTag = $bindable(),
+ createTag = false,
+ saveNewTag,
+ saveEditedTag,
+ isSaving = false
+ } = $props();
+
+ let modalElement;
function open() {
// hide tag picker
document.querySelector('.tooltip').classList.remove('shown');
- let modal = new bootstrap.Modal(document.getElementById('modalTag'));
+ let modal = new bootstrap.Modal(modalElement);
modal.show();
}
function close() {
- let modal = bootstrap.Modal.getInstance(document.getElementById('modalTag'));
+ let modal = bootstrap.Modal.getInstance(modalElement);
modal.hide();
}
}
</script>
-<div class="modal fade" id="modalTag" tabindex="-1">
+<div bind:this={modalElement} class="modal fade" id="modalTag" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
<button
- onclick={saveNewTag}
+ onclick={() => {
+ createTag ? saveNewTag() : saveEditedTag();
+ }}
type="button"
class="btn btn-primary"
disabled={!editTag.name || isSaving}
<script>
- import { fade, blur, slide } from 'svelte/transition';
+ import { blur } from 'svelte/transition';
import axios from 'axios';
//import { dev } from '$app/environment';
import { goto } from '$app/navigation';
import { API_URL } from '$lib/APIurl.js';
import trianglify from 'trianglify';
import { useTrianglify, trianglifyOpacity, autoLoadImages } from '$lib/settingsStore.js';
+ import { tags } from '$lib/tagStore.js';
+ import TagModal from '$lib/TagModal.svelte';
import {
faRightFromBracket,
faGlasses,
faPencil,
- faSliders
+ faSliders,
+ faTriangleExclamation
} from '@fortawesome/free-solid-svg-icons';
+ import Tag from '$lib/Tag.svelte';
let { children } = $props();
let inDuration = 150;
}
}
+ let settingsModal;
+ function openSettingsModal() {
+ settingsModal = new bootstrap.Modal(document.getElementById('settingsModal'));
+ settingsModal.show();
+ }
+
$effect(() => {
if ($trianglifyOpacity) {
if (document.querySelector('canvas')) {
}
});
+ /* Important for development: convenient modal-handling with HMR */
+ if (import.meta.hot) {
+ import.meta.hot.dispose(() => {
+ document.querySelectorAll('.modal-backdrop').forEach((el) => el.remove());
+ });
+ }
+
onMount(() => {
createBackground();
}, 200);
});
});
+
+ let editTagModal;
+ let editTag = $state({});
+ let isSavingEditedTag = $state(false);
+
+ function openTagModal(tagId) {
+ $tags.forEach((tag) => {
+ if (tag.id === tagId) {
+ editTag.name = tag.name;
+ editTag.color = tag.color;
+ editTag.icon = tag.icon;
+ editTag.id = tag.id;
+ return;
+ }
+ });
+
+ settingsModal.hide();
+ editTagModal.open();
+ }
+
+ let deleteTagId = $state(null);
+ function askDeleteTag(tagId) {
+ if (deleteTagId === tagId) deleteTagId = null;
+ else deleteTagId = tagId;
+ }
+
+ let isDeletingTag = $state(false);
+ function deleteTag(tagId) {
+ if (isDeletingTag) return;
+ isDeletingTag = true;
+
+ axios
+ .get(API_URL + '/logs/deleteTag', { params: { id: tagId } })
+ .then((response) => {
+ if (response.data.success) {
+ $tags = $tags.filter((tag) => tag.id !== tagId);
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+
+ // show toast
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorDeleteTag'));
+ toast.show();
+ })
+ .finally(() => {
+ deleteTagId = null;
+ isDeletingTag = false;
+ });
+ }
+
+ function saveEditedTag() {
+ if (isSavingEditedTag) return;
+ isSavingEditedTag = true;
+
+ axios
+ .post(API_URL + '/logs/editTag', editTag)
+ .then((response) => {
+ if (response.data.success) {
+ $tags = $tags.map((tag) => {
+ if (tag.id === editTag.id) {
+ tag.name = editTag.name;
+ tag.color = editTag.color;
+ tag.icon = editTag.icon;
+ }
+ return tag;
+ });
+
+ // show toast
+ const toast = new bootstrap.Toast(document.getElementById('toastSuccessEditTag'));
+ toast.show();
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+
+ // show toast
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorEditTag'));
+ toast.show();
+ })
+ .finally(() => {
+ isSavingEditedTag = false;
+ editTagModal.close();
+ settingsModal.show();
+ });
+ }
</script>
<main class="d-flex flex-column">
</div>
<div class="col-lg-4 col-sm-5 col pe-0 d-flex flex-row justify-content-end">
- <button
- class="btn btn-outline-secondary me-2"
- data-bs-toggle="modal"
- data-bs-target="#settingsModal"><Fa icon={faSliders} /></button
+ <button class="btn btn-outline-secondary me-2" onclick={openSettingsModal}
+ ><Fa icon={faSliders} /></button
>
<button class="btn btn-outline-secondary" onclick={logout}
><Fa icon={faRightFromBracket} /></button
{/key}
</div>
+ <TagModal
+ bind:this={editTagModal}
+ createTag={false}
+ bind:editTag
+ isSaving={isSavingEditedTag}
+ {saveEditedTag}
+ />
+
<!-- Full screen modal -->
<div class="modal fade" data-bs-backdrop="static" id="settingsModal">
<div
blub <br />
</div>
- <div id="tags"><h4>Tags</h4></div>
+ <h3 id="tags" class="text-primary">Tags</h3>
+ <div>
+ Hier können Tags bearbeitet oder auch vollständig aus DailyTxT gelöscht werden.
+ <div class="d-flex flex-column tagColumn mt-1">
+ {#each $tags as tag}
+ <Tag
+ {tag}
+ isEditable
+ editTag={openTagModal}
+ isDeletable
+ deleteTag={askDeleteTag}
+ />
+ {#if deleteTagId === tag.id}
+ <div class="alert alert-danger align-items-center" role="alert">
+ <div>
+ <Fa icon={faTriangleExclamation} fw /> <b>Tag dauerhaft löschen?</b> Dies
+ kann einen Moment dauern, da jeder Eintrag nach potenziellen Verlinkungen
+ durchsucht werden muss. Änderungen werden zudem u. U. erst nach einem Neuladen
+ im Browser angezeigt.
+ </div>
+ <!-- svelte-ignore a11y_consider_explicit_label -->
+ <div class="d-flex flex-row mt-2">
+ <button class="btn btn-secondary" onclick={() => (deleteTagId = null)}
+ >Abbrechen
+ </button>
+ <button
+ disabled={isDeletingTag}
+ class="btn btn-danger ms-3"
+ onclick={() => deleteTag(tag.id)}
+ >Löschen
+ {#if isDeletingTag}
+ <span
+ class="spinner-border spinner-border-sm ms-2"
+ role="status"
+ aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ </div>
+ </div>
+ {/if}
+ {/each}
+ </div>
+ </div>
<div id="templates"><h4>Vorlagen</h4></div>
</div>
</div>
</div>
+
+ <div class="toast-container position-fixed bottom-0 end-0 p-3">
+ <div
+ id="toastSuccessEditTag"
+ class="toast align-items-center text-bg-success"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Änderungen wurden gespeichert!</div>
+ </div>
+ </div>
+
+ <div
+ id="toastErrorEditTag"
+ 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 der Änderungen!</div>
+ </div>
+ </div>
+
+ <div
+ id="toastErrorDeleteTag"
+ 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 Löschen des Tags!</div>
+ </div>
+ </div>
+ </div>
</main>
<style>
+ :global(.tagColumn > span) {
+ width: min-content;
+ }
+
+ .tagColumn {
+ gap: 0.5rem;
+ /* width: min-content; */
+ }
+
#selectMode:checked {
border-color: #da880e;
background-color: #da880e;
});
}
- let editTag = $state({});
+ let newTag = $state({});
let tagModal;
- function openTagModal(tag) {
- if (tag === null) {
- editTag = {
- icon: '',
- name: '',
- color: '#f57c00'
- };
- } else {
- editTag = tag;
- }
+ function openTagModal() {
+ newTag = {
+ icon: '',
+ name: '',
+ color: '#f57c00'
+ };
tagModal.open();
}
isSavingNewTag = true;
axios
.post(API_URL + '/logs/saveNewTag', {
- icon: editTag.icon,
- name: editTag.name,
- color: editTag.color
+ icon: newTag.icon,
+ name: newTag.name,
+ color: newTag.color
})
.then((response) => {
if (response.data.success) {
id="tag-input"
placeholder="Tag..."
/>
- <button
- class="newTagBtn btn btn-outline-secondary ms-2"
- onclick={() => {
- openTagModal(null);
- }}
- >
+ <button class="newTagBtn btn btn-outline-secondary ms-2" onclick={openTagModal}>
<Fa icon={faSquarePlus} fw /> Neu
</button>
</div>
<TagModal
bind:this={tagModal}
- bind:editTag
+ bind:editTag={newTag}
createTag="true"
isSaving={isSavingNewTag}
{saveNewTag}