WebSocket API, new ID/encrypt/decrypt algorithm.

This commit is contained in:
Clément Pons
2025-11-18 17:54:11 +01:00
parent 2a158be3fa
commit 7a40f8abac
26 changed files with 2303 additions and 293 deletions

View File

@@ -1,80 +0,0 @@
<template>
<TreeRoot v-bind="forward" v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 overflow-auto max-h-full">
<DraggableTreeItem v-for="item in flattenItems" :key="item._id" v-bind="item" class="group flex items-center outline-none relative cursor-pointer max-w-full" @select.prevent @toggle.prevent>
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver }">
<slot :handleToggle="handleToggle"
:handleSelect="handleSelect"
:isExpanded="isExpanded"
:isSelected="isSelected"
:isDragging="isDragging"
:isDraggedOver="isDraggedOver"
:item="item"
/>
</template>
<template #hint="{ instruction }">
<div v-if="instruction">
<slot name="hint" :instruction="instruction" />
</div>
</template>
</DraggableTreeItem>
</TreeRoot>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { useForwardPropsEmits, type FlattenedItem, type TreeRootEmits, type TreeRootProps } from 'radix-vue';
import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
const props = defineProps<TreeRootProps<T>>();
const emits = defineEmits<TreeRootEmits<T> & {
'updateTree': [instruction: Instruction, itemId: string, targetId: string];
}>();
defineSlots<{
default: (props: {
handleToggle: () => void,
handleSelect: () => void,
isExpanded: boolean,
isSelected: boolean,
isDragging: boolean,
isDraggedOver: boolean,
item: FlattenedItem<T>,
}) => any,
hint: (props: {
instruction: Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null
}) => any,
}>();
const forward = useForwardPropsEmits(props, emits);
watchEffect((onCleanup) => {
const dndFunction = combine(
monitorForElements({
onDrop(args) {
const { location, source } = args;
if (!location.current.dropTargets.length)
return;
const itemId = source.data.id as string;
const target = location.current.dropTargets[0];
const targetId = target.data.id as string;
const instruction: Instruction | null = extractInstruction(
target.data,
);
if (instruction !== null)
{
emits('updateTree', instruction, itemId, targetId);
}
},
}),
)
onCleanup(() => {
dndFunction();
})
})
</script>

View File

@@ -1,140 +0,0 @@
<template>
<TreeItem ref="el" v-bind="forward" v-slot="{ isExpanded, isSelected, isIndeterminate, handleToggle, handleSelect }">
<slot
:is-expanded="isExpanded"
:is-selected="isSelected"
:is-indeterminate="isIndeterminate"
:handle-select="handleSelect"
:handle-toggle="handleToggle"
:isDragging="isDragging"
:isDraggedOver="isDraggedOver"
/>
<div v-if="instruction">
<slot name="hint" :instruction="instruction" />
</div>
</TreeItem>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { useForwardPropsEmits, type FlattenedItem, type TreeItemEmits, type TreeItemProps } from 'radix-vue';
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
const props = defineProps<TreeItemProps<T> & FlattenedItem<T>>();
const emits = defineEmits<TreeItemEmits<T>>();
defineSlots<{
default: (props: {
isExpanded: boolean
isSelected: boolean
isIndeterminate: boolean | undefined
isDragging: boolean
isDraggedOver: boolean
handleToggle: () => void
handleSelect: () => void
}) => any,
hint: (props: {
instruction: Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null
}) => any,
}>()
const forward = useForwardPropsEmits(props, emits);
const element = templateRef('el');
const isDragging = ref(false);
const isDraggedOver = ref(false);
const isInitialExpanded = ref(false);
const instruction = ref<Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null>(null);
const mode = computed(() => {
if (props.hasChildren)
return 'expanded'
if (props.index + 1 === props.parentItem?.children?.length)
return 'last-in-group'
return 'standard'
});
watchEffect((onCleanup) => {
const currentElement = unrefElement(element) as HTMLElement;
if (!currentElement)
return
const item = { ...props.value, level: props.level, id: props._id }
const expandItem = () => {
if (!element.value?.isExpanded) {
element.value?.handleToggle()
}
}
const closeItem = () => {
if (element.value?.isExpanded) {
element.value?.handleToggle()
}
}
const dndFunction = combine(
draggable({
element: currentElement,
getInitialData: () => item,
onDragStart: () => {
isDragging.value = true
isInitialExpanded.value = element.value?.isExpanded ?? false
closeItem()
},
onDrop: () => {
isDragging.value = false
if (isInitialExpanded.value)
expandItem()
},
}),
dropTargetForElements({
element: currentElement,
getData: ({ input, element }) => {
const data = { id: item.id }
return attachInstruction(data, {
input,
element,
indentPerLevel: 16,
currentLevel: props.level,
mode: mode.value,
block: [],
})
},
canDrop: ({ source }) => {
return source.data.id !== item.id
},
onDrag: ({ self }) => {
instruction.value = extractInstruction(self.data) as typeof instruction.value
},
onDragEnter: ({ source }) => {
if (source.data.id !== item.id) {
isDraggedOver.value = true
}
},
onDragLeave: () => {
isDraggedOver.value = false
instruction.value = null
},
onDrop: ({ location }) => {
isDraggedOver.value = false
instruction.value = null
},
getIsSticky: () => true,
}),
monitorForElements({
canMonitor: ({ source }) => {
return source.data.id !== item.id
},
}),
)
// Cleanup dnd function
onCleanup(() => dndFunction())
})
</script>

View File

@@ -84,10 +84,10 @@ export const characterChoicesTable = table("character_choices", {
export const campaignTable = table("campaign", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
description: text(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
link: text().notNull(),
status: text({ enum: ['PREPARING', 'PLAYING', 'ARCHIVED'] }).default('PREPARING'),
settings: text({ mode: 'json' }).default('{}'),
inventory: text({ mode: 'json' }).default('[]'),
money: int().default(0),
public_notes: text().default(''),
@@ -103,11 +103,11 @@ export const campaignCharactersTable = table("campaign_characters", {
}, (table) => [primaryKey({ columns: [table.id, table.character] })]);
export const campaignLogsTable = table("campaign_logs", {
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
from: int().references(() => campaignCharactersTable.character, { onDelete: 'cascade', onUpdate: 'cascade' }),
target: int().references(() => campaignCharactersTable.character, { onDelete: 'cascade', onUpdate: 'cascade' }),
timestamp: int({ mode: 'timestamp_ms' }).notNull(),
type: text({ enum: ['ITEM', 'CHARACTER', 'PLACE', 'EVENT', 'FIGHT', 'TEXT'] }),
details: text().notNull(),
}, (table) => [primaryKey({ columns: [table.id, table.from, table.timestamp] })]);
}, (table) => [primaryKey({ columns: [table.id, table.target, table.timestamp] })]);
export const usersRelation = relations(usersTable, ({ one, many }) => ({
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
@@ -166,5 +166,5 @@ export const campaignCharacterRelation = relations(campaignCharactersTable, ({ o
}));
export const campaignLogsRelation = relations(campaignLogsTable, ({ one }) => ({
campaign: one(campaignTable, { fields: [campaignLogsTable.id], references: [campaignTable.id], }),
character: one(campaignCharactersTable, { fields: [campaignLogsTable.from], references: [campaignCharactersTable.character], }),
character: one(campaignCharactersTable, { fields: [campaignLogsTable.target], references: [campaignCharactersTable.character], }),
}));

View File

@@ -16,6 +16,10 @@ onMounted(() => {
{
const campaign = new CampaignSheet(id, user);
container.value.appendChild(campaign.container);
onUnmounted(() => {
campaign.ws?.close();
})
}
});
})

View File

@@ -38,7 +38,7 @@ function create()
{
useRequestFetch()('/api/campaign', {
method: 'POST',
body: { id: 'new', name: 'Test', description: '', joinby: 'link' },
body: { name: 'Margooning', public_notes: '', dm_notes: '', settings: {} },
}).then(() => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' }))
}
</script>
@@ -52,6 +52,7 @@ function create()
<Loading size="large" />
</div>
<template v-else-if="status === 'success' && loggedIn && user">
<div class="flex flex-row items-center w-full"><Button @click="() => create()">Nouvelle campagne</Button></div>
<div v-if="campaigns && campaigns.length > 0" class="flex flex-col gap-4">
<div v-if="valids && valids.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of valids">

View File

@@ -28,6 +28,10 @@ onMounted(() => {
{
const character = new CharacterSheet(id, user);
container.value.appendChild(character.container);
onUnmounted(() => {
character.ws?.close();
})
}
});
});

View File

@@ -19,7 +19,7 @@ export type Campaign = {
logs: CampaignLog[];
} & CampaignVariables;
export type CampaignLog = {
from: number;
target: number;
timestamp: Serialize<Date>;
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'EVENT' | 'FIGHT' | 'TEXT';
details: string;

View File

@@ -42,6 +42,8 @@ export type Character = {
owner: number;
username?: string;
visibility: "private" | "public";
campaign?: number;
};
export type CharacterVariables = {
health: number;