diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index b6c9ce7..a6d84de 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -13,6 +13,23 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Install SSH client
+ run: |
+ apt-get update -y && apt-get install -y openssh-client
+
+ - name: Setup SSH inside container
+ run: |
+ mkdir -p ~/.ssh
+ echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+
+ # Add the container host to known_hosts
+ ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
+
+ - name: Log in to registry
+ run: |
+ echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.aranroig.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
+
- name: Build frontend
run: |
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-frontend:latest ./frontend
@@ -23,17 +40,17 @@ jobs:
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-backend:latest ./backend
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/dragonroll-backend:latest
- # - name: Copy files
- # run: |
- # scp docker-compose.yml deploy@${{ secrets.DEPLOY_HOST}}:/var/www/app/
- # scp nginx.conf deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/nginx.conf
+ - name: Copy files
+ run: |
+ scp docker-compose.yml deploy@${{ secrets.DEPLOY_HOST}}:/var/www/app/
+ scp nginx.conf deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/nginx.conf
- #- name: Deploy
- # run: |
- # ssh deploy@${{ secrets.DEPLOY_HOST }} << 'EOF'
- # echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.aranroig.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- # cd /var/www/app/
- # docker-compose pull
- # docker-compose up -d
- # EOF
+ - name: Deploy
+ run: |
+ ssh deploy@${{ secrets.DEPLOY_HOST }} << 'EOF'
+ echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.aranroig.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
+ cd /var/www/app/
+ docker-compose pull
+ docker-compose up -d
+ EOF
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a76cc8c
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,21 @@
+version: "3.9"
+
+services:
+ nginx:
+ image: nginx:latest
+ ports:
+ - "3000:80"
+ volumes:
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ depends_on:
+ - frontend
+ - backend
+ restart: always
+
+ frontend:
+ image: git.aranroig.com/syndria98/dragonroll-frontend:latest
+ restart: always
+
+ backend:
+ image: git.aranroig.com/syndria98/dragonroll-backend:latest
+ restart: always
\ No newline at end of file
diff --git a/frontend/app/components/viewer/content/Note.vue b/frontend/app/components/viewer/content/Note.vue
index e681e7b..4340942 100644
--- a/frontend/app/components/viewer/content/Note.vue
+++ b/frontend/app/components/viewer/content/Note.vue
@@ -2,7 +2,7 @@
import { onMounted, onUnmounted, ref, createApp } from 'vue';
import ToastManager from '~/components/managers/ToastManager.vue';
import { emitter } from '~/services/Emitter';
-import { ParseMarkdown } from '~/services/Marker';
+import { GetWidget, ParseMarkdown } from '~/services/Marker';
import Server from '~/services/Server';
import { DisplayToast } from '~/services/Toaster';
import TestWidget from '../widgets/TestWidget.vue';
@@ -15,6 +15,9 @@ const sourceText = ref(''); // Original markdown source, used for editing
const displayText = ref(''); // Compiled HTML from markdown
const editingMode = ref(false);
+const editableTitle = ref(null);
+const title = ref(props.title);
+const displayTitle = ref('');
function closeNote(){
emitter.emit('delete-note', props.noteKey);
@@ -28,12 +31,9 @@ function mountComponents() {
const nodes = document.querySelectorAll('.vue-component');
nodes.forEach(el => {
- const app = createApp(TestWidget, { content: el.dataset.content });
+ const app = createApp(GetWidget(el.dataset.component), { content: el.dataset.content });
app.mount(el);
- console.log("Mounted a component")
});
-
- console.log("Huh")
}
///
@@ -51,6 +51,8 @@ watch(sourceText, (newText) => {
onMounted(() => {
sourceText.value = props.text;
+ title.value = props.title;
+ displayTitle.value = props.title;
// window.addEventListener('keydown', handleKeydown);
setTimeout(() => setupCallout(), 0);
update();
@@ -65,7 +67,10 @@ function handleKeydown(e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'e') {
e.preventDefault(); // prevent browser default behavior
editingMode.value = !editingMode.value;
- if(!editingMode.value) update();
+ if(!editingMode.value){
+ update();
+ SaveNote(); // Save when switching to display mode
+ }
return;
}
@@ -80,13 +85,14 @@ function handleKeydown(e) {
function SaveNote(){
Server().post('/note/update', {
id: props.noteKey,
- content: sourceText.value
+ content: sourceText.value,
+ title: title.value,
}).then((response) => {
if(response.data.status !== 'ok'){
// Handle error (e.g., show a notification)
return;
}
- DisplayToast('green', "Note saved successfully.", 500);
+ // DisplayToast('green', "Note saved successfully.", 500);
}).catch((error) => {
// Handle error (e.g., show a notification)
});
@@ -138,6 +144,10 @@ function setupCallout() {
}
+const editTitle = (e) => {
+ title.value = e.target.innerText;
+}
+
@@ -150,7 +160,11 @@ function setupCallout() {
diff --git a/frontend/app/components/viewer/widgets/RollWidget.vue b/frontend/app/components/viewer/widgets/RollWidget.vue
new file mode 100644
index 0000000..649f7c8
--- /dev/null
+++ b/frontend/app/components/viewer/widgets/RollWidget.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/app/components/windows/CreateCampaignWindow.vue b/frontend/app/components/windows/CreateCampaignWindow.vue
index 8072212..f748b6d 100644
--- a/frontend/app/components/windows/CreateCampaignWindow.vue
+++ b/frontend/app/components/windows/CreateCampaignWindow.vue
@@ -12,6 +12,7 @@ const wrapper = ref(null);
const props = defineProps(['data']);
const data = props.data;
+const loading = ref(false);
let id = data.id;
@@ -29,11 +30,13 @@ const colorPicker = ref(null);
function NewCampaign(){
const color = colorPicker.value.GetColor();
console.log(color);
+ loading.value = true;
Server().post('/campaign/create', {
name: campaignName.value,
description: campaignDescription.value,
color: colorPicker.value.GetColor(),
}).then((response) => {
+ loading.value = false;
console.log(response.data);
DisplayToast('green', $t('campaigns.create.success'), 3000);
ClearWindow({id});
@@ -63,7 +66,12 @@ function NewCampaign(){
diff --git a/frontend/app/services/Marker.js b/frontend/app/services/Marker.js
index 6bfea80..7d87c6a 100644
--- a/frontend/app/services/Marker.js
+++ b/frontend/app/services/Marker.js
@@ -1,15 +1,25 @@
import { Marked } from "marked";
-
-const marker = new Marked();
-
-// optional: use a shared marked instance inside renderer
-const renderMarkdown = (text) => marker.parse(text);
-
-const WidgetMap = {
+const widget_map = {
test: () => import("~/components/viewer/widgets/TestWidget.vue"),
table: () => import("~/components/viewer/widgets/TableWidget.vue"),
+ roll: () => import("~/components/viewer/widgets/RollWidget.vue"),
};
+const componentCache = {}
+
+const GetWidget = (type) => {
+ if (!componentCache[type]) {
+ componentCache[type] = defineAsyncComponent(
+ widget_map[type]
+ )
+ }
+
+ return componentCache[type]
+}
+
+
+const marker = new Marked();
+
const extension = {
name: "something",
level: "block",
@@ -33,12 +43,37 @@ const extension = {
},
};
+const inlineExtension = {
+ name: "something_inline",
+ level: "inline",
+ start(src) {
+ return src.indexOf("@");
+ },
+
+ tokenizer(src) {
+ const rule = /^@(\w+)\s*\[([^\]]*)\]/;
+ const match = rule.exec(src);
+ if (!match) return;
+
+ return {
+ type: "something_inline",
+ raw: match[0],
+ name: match[1],
+ text: match[2],
+ };
+ },
+
+ renderer(token) {
+ return ``;
+ },
+};
+
marker.use({
- extensions: [extension],
+ extensions: [extension, inlineExtension],
});
function ParseMarkdown(source) {
return marker.parse(source || "");
}
-export { ParseMarkdown, WidgetMap };
\ No newline at end of file
+export { ParseMarkdown, GetWidget };
\ No newline at end of file
diff --git a/frontend/app/services/widgets/DiceParser.js b/frontend/app/services/widgets/DiceParser.js
new file mode 100644
index 0000000..6022fd0
--- /dev/null
+++ b/frontend/app/services/widgets/DiceParser.js
@@ -0,0 +1,130 @@
+// dice-parser.js
+
+function roll(sides) {
+ return Math.floor(Math.random() * sides) + 1;
+}
+
+function tokenize(expr) {
+ const re = /(\d*d\d+(?:adv|dis|kh\d+|kl\d+)?|\d+|[+\-*\/()])/gi;
+ const tokens = [];
+ let m;
+ while ((m = re.exec(expr)) !== null) tokens.push(m[0].toLowerCase());
+ return tokens;
+}
+
+function parseDiceToken(tok) {
+ const m = tok.match(/^(\d*)d(\d+)(adv|dis|kh(\d+)|kl(\d+))?$/i);
+ if (!m) return null;
+ const count = parseInt(m[1] || '1');
+ const sides = parseInt(m[2]);
+ const mod = (m[3] || '').toLowerCase();
+ if (count < 1 || count > 1000 || sides < 2 || sides > 10000)
+ throw new Error(`Invalid dice: ${tok}`);
+ return { count, sides, mod };
+}
+
+export function parse(expr) {
+ const tokens = tokenize(expr);
+ if (!tokens.length) throw new Error('Empty expression');
+ let pos = 0;
+ const rolls = [];
+
+ function peek() { return tokens[pos]; }
+ function consume() { return tokens[pos++]; }
+
+ function parseExpr() { return parseAddSub(); }
+
+ function parseAddSub() {
+ let left = parseMulDiv();
+ while (peek() === '+' || peek() === '-') {
+ const op = consume();
+ const right = parseMulDiv();
+ left = {
+ value: op === '+' ? left.value + right.value : left.value - right.value,
+ rolls: [...left.rolls, ...right.rolls],
+ };
+ }
+ return left;
+ }
+
+ function parseMulDiv() {
+ let left = parseUnary();
+ while (peek() === '*' || peek() === '/') {
+ const op = consume();
+ const right = parseUnary();
+ if (op === '/' && right.value === 0) throw new Error('Division by zero');
+ left = {
+ value: op === '*' ? left.value * right.value : Math.floor(left.value / right.value),
+ rolls: [...left.rolls, ...right.rolls],
+ };
+ }
+ return left;
+ }
+
+ function parseUnary() {
+ if (peek() === '-') {
+ consume();
+ const r = parsePrimary();
+ return { value: -r.value, rolls: r.rolls };
+ }
+ return parsePrimary();
+ }
+
+ function parsePrimary() {
+ const tok = peek();
+ if (!tok) throw new Error('Unexpected end of expression');
+
+ if (tok === '(') {
+ consume();
+ const inner = parseExpr();
+ if (peek() !== ')') throw new Error('Missing closing )');
+ consume();
+ return inner;
+ }
+
+ const diceInfo = parseDiceToken(tok);
+ if (diceInfo) {
+ consume();
+ return rollDice(diceInfo);
+ }
+
+ if (/^\d+$/.test(tok)) {
+ consume();
+ return { value: parseInt(tok), rolls: [] };
+ }
+
+ throw new Error(`Unexpected token: ${tok}`);
+ }
+
+ function rollDice({ count, sides, mod }) {
+ let rawRolls, kept;
+
+ if (mod === 'adv') {
+ rawRolls = [roll(sides), roll(sides)];
+ kept = [Math.max(...rawRolls)];
+ } else if (mod === 'dis') {
+ rawRolls = [roll(sides), roll(sides)];
+ kept = [Math.min(...rawRolls)];
+ } else if (mod.startsWith('kh')) {
+ const k = parseInt(mod.slice(2));
+ rawRolls = Array.from({ length: count }, () => roll(sides));
+ kept = [...rawRolls].sort((a, b) => b - a).slice(0, k);
+ } else if (mod.startsWith('kl')) {
+ const k = parseInt(mod.slice(2));
+ rawRolls = Array.from({ length: count }, () => roll(sides));
+ kept = [...rawRolls].sort((a, b) => a - b).slice(0, k);
+ } else {
+ rawRolls = Array.from({ length: count }, () => roll(sides));
+ kept = rawRolls.slice();
+ }
+
+ const entry = { sides, rawRolls, kept, mod, value: kept.reduce((a, b) => a + b, 0) };
+ rolls.push(entry);
+ return { value: entry.value, rolls: [entry] };
+ }
+
+ const result = parseExpr();
+ if (pos < tokens.length) throw new Error(`Unexpected token: ${tokens[pos]}`);
+
+ return { total: result.value, rolls: result.rolls };
+}
\ No newline at end of file