ye
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 52s

This commit is contained in:
2026-04-29 01:32:09 +02:00
parent 76bb9fbb30
commit e6d66529e3
16 changed files with 767 additions and 83 deletions

View File

@@ -1,17 +1,41 @@
<script setup>
import TopSearchBar from './topbar/TopSearchBar.vue';
import { useCampaignService } from '~/services/Campaign';
import { CreateWindow } from '~/services/Windows';
const { Campaign } = useCampaignService();
const campaignName = computed(() => {
return Campaign.value?.name ?? 'Campaign';
});
function openCreateNoteWindow() {
if (!Campaign.value) {
return;
}
CreateWindow('create_note');
}
</script>
<template>
<div class="top-bar">
<div class="left">
<span class="top-bar-title"></span>
<span class="top-bar-title">
<img src="/img/logo.png" alt="Dragonroll Logo" class="logo">
<span>{{ campaignName }}</span>
</span>
</div>
<div class="center">
<TopSearchBar></TopSearchBar>
</div>
<div class="right"></div>
<div class="right">
<button class="note-button sound-click" type="button" @click="openCreateNoteWindow" :disabled="!Campaign">
<img class="note-button-icon" src="/icons/iconoir/regular/plus.svg" alt="" aria-hidden="true">
<span>New Note</span>
</button>
</div>
</div>
</template>
@@ -25,17 +49,51 @@ import TopSearchBar from './topbar/TopSearchBar.vue';
display: flex;
}
.logo {
height: 36px;
width: 36px;
position: absolute;
top: 0px;
left: 8px;
}
.left, .right {
flex: 1;
}
.right {
text-align: right;
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 10px;
}
.top-bar-title {
padding: 10px;
display: flex;
margin-left: 48px;
font-weight: bold;
}
</style>
.note-button {
height: 30px;
padding: 0 12px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-background-soft);
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.note-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.note-button-icon {
width: 16px;
height: 16px;
}
</style>

View File

@@ -1,16 +1,13 @@
<script setup>
import NoteContainer from './NoteContainer.vue';
const emitter = useEmitter();
function hideSearch(){
emitter.emit("hide-search-container");
}
</script>
<template>
<div class="content" v-on:click="hideSearch">
<NoteContainer></NoteContainer>
<div class="content">
<NoteContainer>
</NoteContainer>
<!-- PowerMod -->
</div>
</template>
@@ -18,6 +15,7 @@ function hideSearch(){
<style scoped>
.content {
flex-grow: 1;
min-width: 0; /* 👈 important */
display: flex;
justify-content: center;
align-items: center;

View File

@@ -1,56 +1,73 @@
<script setup>
import { onMounted, ref } from 'vue';
import { marked } from 'marked';
import { onMounted, onUnmounted, ref } from 'vue';
import ToastManager from '~/components/managers/ToastManager.vue';
import { emitter } from '~/services/Emitter';
import Server from '~/services/Server';
import { DisplayToast } from '~/services/Toaster';
// import { GetNote, GetContent } from '@/services/Content';
const props = defineProps(['text', 'title', 'noteKey']);
const noteContent = ref(null); // Markdown text
const noteContent = ref(null);
const sourceText = ref(''); // Original markdown source, used for editing
const displayText = ref(''); // Compiled HTML from markdown
const emitter = useEmitter();
function gotoNote(){
// emitter.emit('goto-note', props.noteKey);
}
function closeNote(){
// emitter.emit('delete-note', props.noteKey);
}
/*
onMounted(() => {
let content = GetContent();
let elements = noteContent.value.getElementsByTagName('a');
for(let i = 0, len = elements.length; i < len; i++) {
let link = elements[i].pathname.split('/').slice(1).join('');
link = decodeURIComponent(link);
if(content[link] !== undefined){
elements[i].onclick = function (event) {
event.preventDefault();
GetNote(link, (result) => {
emitter.emit("push-note", {key: link, text: "<h1>" + result.title + "</h1>" + result.html, title: result.title});
});
return false;
}
} else {
elements[i].classList.add("error-link");
elements[i].onclick = function (event) {
event.preventDefault();
return false;
}
}
}
setTimeout(() => setupCallout(), 0);
});
const editingMode = ref(false);
function closeNote(){
emitter.emit('delete-note', props.noteKey);
}
function gotoNote(){
// emitter.emit('goto-note', props.noteKey);
const compiledMarkdown = computed(() => {
return marked.parse(sourceText.value);
});
watch(sourceText, (newText) => {
displayText.value = compiledMarkdown.value;
setTimeout(() => setupCallout(), 0);
});
onMounted(() => {
sourceText.value = props.text;
// window.addEventListener('keydown', handleKeydown);
setTimeout(() => setupCallout(), 0);
});
onUnmounted(() => {
// window.removeEventListener('keydown', handleKeydown);
});
function handleKeydown(e) {
// Check for Ctrl + E (or Cmd + E on Mac)
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'e') {
e.preventDefault(); // prevent browser default behavior
editingMode.value = !editingMode.value;
return;
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
e.preventDefault(); // prevent browser default behavior
// Save the note (you can emit an event or call a method here)
SaveNote();
return;
}
}
function SaveNote(){
Server().post('/note/update', {
id: props.noteKey,
content: sourceText.value
}).then((response) => {
if(response.data.status !== 'ok'){
// Handle error (e.g., show a notification)
return;
}
DisplayToast('green', "Note saved successfully.", 500);
}).catch((error) => {
// Handle error (e.g., show a notification)
});
}
function toggleCallout() {
@@ -78,6 +95,10 @@ function toggleCallout() {
}
function setupCallout() {
if (!noteContent.value) {
return;
}
const collapsible = noteContent.value.getElementsByClassName(
`callout is-collapsible`,
);
@@ -93,21 +114,21 @@ function setupCallout() {
}
}
}
*/
</script>
<template>
<div class="note">
<div class="note-stunt" v-on:click="gotoNote">
<div class="note" @keydown="handleKeydown" tabindex="0">
<div class="note-stunt">
<div class="close-button" v-on:click="closeNote">
<img class="icon" src="/icons/Pixelarticons/white/close.svg" alt="My Happy SVG"/>
</div>
<span>{{ title }}</span>
</div>
<div class="note-content-container">
<div class="note-content" ref="noteContent" v-html="text"></div>
<textarea v-model="sourceText" class="full-editor" v-if="editingMode"></textarea>
<div class="note-content" ref="noteContent" v-html="displayText" v-else></div>
</div>
</div>
</template>
@@ -124,6 +145,22 @@ function setupCallout() {
user-select: none;
}
.full-editor {
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
outline: none;
resize: none;
padding: 20px;
font-size: 16px;
font-family: monospace; /* optional, gives document/editor feel */
padding-bottom: 400px; /* Small bottom margin */
}
.close-button {
height: 20px;
width: 20px;
@@ -131,6 +168,7 @@ function setupCallout() {
display: flex;
justify-content: center;
cursor: pointer;
filter: invert(var(--color-icon-invert));
}
.note {
@@ -138,34 +176,32 @@ function setupCallout() {
max-width: 700px;
overflow-y: auto;
border-color: var(--note-border-color);
border-color: var(--color-note-border);
border-width: 0px;
border-right-width: 1px;
border-style: solid;
display: flex;
background-color: var(--background-color);
background-color: var(--color-background);
position: sticky;
top: 0px;
}
/* Contingut de cada nota */
.note-content {
padding-bottom: 60px;
padding-bottom: 400px;
overflow-y: auto;
max-width: 600px;
padding: 20px;
}
.note-content-container {
margin: 20px;
width: 100%;
}
.note-content :deep(img) {
max-width: 100%;
height: auto;
display: block; /* optional: avoids inline spacing issues */
}
</style>
<style>
.note-content > h1 {
text-align: center;
}
.note-content .katex-display {
max-width: 600px;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue';
import { onMounted, onUnmounted, ref } from 'vue';
import Note from './Note.vue';
import { emitter } from '~/services/Emitter';
@@ -9,6 +9,10 @@ const noteContainer = ref(null);
function calculateContainerWidth(){
let dom = noteContainer.value;
if (!dom) {
return;
}
dom.style.width = noteData.value.length * 701 + "px";
setTimeout(() => {
@@ -22,19 +26,33 @@ function calculateContainerWidth(){
}
function pushNote(note){
noteData.value = noteData.value.filter((currentNote) => {
return currentNote.key !== note.key;
});
noteData.value.push(note);
calculateContainerWidth();
}
emitter.on("push-note", (note) => {
function handlePushNote(note) {
pushNote(note);
})
}
emitter.on("delete-note", (key) => {
function handleDeleteNote(key) {
noteData.value = noteData.value.filter((note) => {
return note.key !== key;
});
calculateContainerWidth();
}
// Moure aixo
onMounted(() => {
emitter.on("push-note", handlePushNote);
emitter.on("delete-note", handleDeleteNote);
});
onUnmounted(() => {
emitter.off("push-note", handlePushNote);
emitter.off("delete-note", handleDeleteNote);
});
</script>
@@ -59,10 +77,9 @@ emitter.on("delete-note", (key) => {
display: flex;
height: 100%;
margin: 0;
height: 100%;
background-color: var(--color-background);
}
</style>
<style>
</style>
</style>