You've already forked obsidian-visualiser
Compare commits
7 Commits
dev_fix
...
d208049989
| Author | SHA1 | Date | |
|---|---|---|---|
| d208049989 | |||
|
|
6db6a4b19d | ||
|
|
fde752b6ed | ||
|
|
1c3211d28e | ||
|
|
ab36eec4de | ||
|
|
b9970ccdf8 | ||
|
|
73b0fdf3f5 |
7
app.vue
7
app.vue
@@ -33,6 +33,13 @@ onBeforeMount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
iconify-icon
|
||||||
|
{
|
||||||
|
display: inline-block;
|
||||||
|
width: attr(width px, 1rem);
|
||||||
|
height: attr(height px, 1rem);
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
.ToastRoot[data-type='error'] {
|
.ToastRoot[data-type='error'] {
|
||||||
@apply border-light-red;
|
@apply border-light-red;
|
||||||
@apply dark:border-dark-red;
|
@apply dark:border-dark-red;
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Content } from '~/shared/content.util';
|
|
||||||
import type { ExploreContent, ContentComposable, TreeItem } from '~/types/content';
|
|
||||||
|
|
||||||
const useContentState = () => useState<ExploreContent[]>('content', () => []);
|
|
||||||
|
|
||||||
export function useContent(): ContentComposable {
|
|
||||||
const contentState = useContentState();
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: contentState,
|
|
||||||
tree: computed(() => {
|
|
||||||
const arr: TreeItem[] = [];
|
|
||||||
for(const element of contentState.value)
|
|
||||||
{
|
|
||||||
addChild(arr, element);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}),
|
|
||||||
fetch,
|
|
||||||
get,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetch(force: boolean = false) {
|
|
||||||
const content = useContentState();
|
|
||||||
if(content.value.length === 0 || force)
|
|
||||||
content.value = await useRequestFetch()('/api/file/overview');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function get(path: string, force: boolean = false): Promise<ExploreContent | undefined> {
|
|
||||||
const content = useContentState()
|
|
||||||
const value = content.value;
|
|
||||||
const item = value.find(e => e.path === path);
|
|
||||||
|
|
||||||
if(item && !item.content)
|
|
||||||
{
|
|
||||||
item.content = await useRequestFetch()(`/api/file/content/${encodeURIComponent(path)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
content.value = value;
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addChild(arr: TreeItem[], e: ExploreContent): void {
|
|
||||||
const parent = arr.find(f => e.path.startsWith(f.path));
|
|
||||||
|
|
||||||
if(parent)
|
|
||||||
{
|
|
||||||
if(!parent.children)
|
|
||||||
parent.children = [];
|
|
||||||
|
|
||||||
addChild(parent.children, e);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
arr.push({ ...e });
|
|
||||||
arr.sort((a, b) => {
|
|
||||||
if(a.order !== b.order)
|
|
||||||
return a.order - b.order;
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
44
db/schema.ts
44
db/schema.ts
@@ -8,19 +8,16 @@ export const usersTable = table("users", {
|
|||||||
hash: text().notNull().unique(),
|
hash: text().notNull().unique(),
|
||||||
state: int().notNull().default(0),
|
state: int().notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersDataTable = table("users_data", {
|
export const usersDataTable = table("users_data", {
|
||||||
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userSessionsTable = table("user_sessions", {
|
export const userSessionsTable = table("user_sessions", {
|
||||||
id: int().notNull(),
|
id: int().notNull(),
|
||||||
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
|
}, (table) => [primaryKey({ columns: [table.id, table.user_id] })]);
|
||||||
|
|
||||||
export const userPermissionsTable = table("user_permissions", {
|
export const userPermissionsTable = table("user_permissions", {
|
||||||
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
permission: text().notNull(),
|
permission: text().notNull(),
|
||||||
@@ -37,7 +34,6 @@ export const projectFilesTable = table("project_files", {
|
|||||||
order: int().notNull(),
|
order: int().notNull(),
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const projectContentTable = table("project_content", {
|
export const projectContentTable = table("project_content", {
|
||||||
id: text().primaryKey(),
|
id: text().primaryKey(),
|
||||||
content: blob({ mode: 'buffer' }),
|
content: blob({ mode: 'buffer' }),
|
||||||
@@ -62,38 +58,51 @@ export const characterTable = table("character", {
|
|||||||
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
||||||
thumbnail: blob(),
|
thumbnail: blob(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const characterTrainingTable = table("character_training", {
|
export const characterTrainingTable = table("character_training", {
|
||||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(),
|
stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(),
|
||||||
level: int().notNull(),
|
level: int().notNull(),
|
||||||
choice: int().notNull(),
|
choice: int().notNull(),
|
||||||
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
|
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
|
||||||
|
|
||||||
export const characterLevelingTable = table("character_leveling", {
|
export const characterLevelingTable = table("character_leveling", {
|
||||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
level: int().notNull(),
|
level: int().notNull(),
|
||||||
choice: int().notNull(),
|
choice: int().notNull(),
|
||||||
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
|
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
|
||||||
|
|
||||||
export const characterAbilitiesTable = table("character_abilities", {
|
export const characterAbilitiesTable = table("character_abilities", {
|
||||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(),
|
ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(),
|
||||||
value: int().notNull().default(0),
|
value: int().notNull().default(0),
|
||||||
max: int().notNull().default(0),
|
max: int().notNull().default(0),
|
||||||
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
|
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
|
||||||
|
|
||||||
export const characterChoicesTable = table("character_choices", {
|
export const characterChoicesTable = table("character_choices", {
|
||||||
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
id: text().notNull(),
|
id: text().notNull(),
|
||||||
choice: int().notNull(),
|
choice: int().notNull(),
|
||||||
}, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]);
|
}, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]);
|
||||||
|
|
||||||
|
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' }),
|
||||||
|
});
|
||||||
|
export const campaignMembersTable = table("campaign_members", {
|
||||||
|
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
user: int().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
rights: text({ enum: [ 'player', 'dm' ] }),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.id, table.user] })]);
|
||||||
|
export const campaignCharactersTable = table("campaign_characters", {
|
||||||
|
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
character: int().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.id, table.character] })]);
|
||||||
|
|
||||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||||
session: many(userSessionsTable),
|
session: many(userSessionsTable),
|
||||||
permission: many(userPermissionsTable),
|
permission: many(userPermissionsTable),
|
||||||
files: many(projectFilesTable),
|
files: many(projectFilesTable),
|
||||||
|
characters: many(characterTable),
|
||||||
}));
|
}));
|
||||||
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||||
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
||||||
@@ -107,14 +116,15 @@ export const userPermissionsRelation = relations(userPermissionsTable, ({ one })
|
|||||||
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
|
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
|
||||||
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const characterRelation = relations(characterTable, ({ one, many }) => ({
|
export const characterRelation = relations(characterTable, ({ one, many }) => ({
|
||||||
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
|
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
|
||||||
training: many(characterTrainingTable),
|
training: many(characterTrainingTable),
|
||||||
levels: many(characterLevelingTable),
|
levels: many(characterLevelingTable),
|
||||||
abilities: many(characterAbilitiesTable),
|
abilities: many(characterAbilitiesTable),
|
||||||
choices: many(characterChoicesTable)
|
choices: many(characterChoicesTable),
|
||||||
|
campaign: one(campaignCharactersTable, { fields: [characterTable.id], references: [campaignCharactersTable.character], }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
|
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
|
||||||
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
|
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
|
||||||
}));
|
}));
|
||||||
@@ -127,3 +137,17 @@ export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({
|
|||||||
export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
|
export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
|
||||||
character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] })
|
character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] })
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const campaignRelation = relations(campaignTable, ({ one, many }) => ({
|
||||||
|
members: many(campaignMembersTable),
|
||||||
|
characters: many(campaignCharactersTable),
|
||||||
|
owner: one(usersTable),
|
||||||
|
}));
|
||||||
|
export const campaignMembersRelation = relations(campaignMembersTable, ({ one }) => ({
|
||||||
|
campaign: one(campaignTable, { fields: [campaignMembersTable.id], references: [campaignTable.id], }),
|
||||||
|
member: one(usersTable, { fields: [campaignMembersTable.user], references: [usersTable.id], })
|
||||||
|
}))
|
||||||
|
export const campaignCharacterRelation = relations(campaignCharactersTable, ({ one }) => ({
|
||||||
|
campaign: one(campaignTable, { fields: [campaignCharactersTable.id], references: [campaignTable.id], }),
|
||||||
|
character: one(characterTable, { fields: [campaignCharactersTable.character], references: [characterTable.id], })
|
||||||
|
}))
|
||||||
24
drizzle/0018_friendly_deadpool.sql
Normal file
24
drizzle/0018_friendly_deadpool.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE `campaign_characters` (
|
||||||
|
`id` integer,
|
||||||
|
`character` integer,
|
||||||
|
PRIMARY KEY(`id`, `character`),
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`character`) REFERENCES `character`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `campaign_members` (
|
||||||
|
`id` integer,
|
||||||
|
`user` integer,
|
||||||
|
`rights` text,
|
||||||
|
PRIMARY KEY(`id`, `user`),
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `campaign`(`id`) ON UPDATE cascade ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`user`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `campaign` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`owner` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
886
drizzle/meta/0018_snapshot.json
Normal file
886
drizzle/meta/0018_snapshot.json
Normal file
@@ -0,0 +1,886 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "42b1cd62-a77f-4fd3-b271-c44a66a56316",
|
||||||
|
"prevId": "153969ef-bcdb-4bbd-bd57-01fbd8004fc6",
|
||||||
|
"tables": {
|
||||||
|
"campaign_characters": {
|
||||||
|
"name": "campaign_characters",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"campaign_characters_id_campaign_id_fk": {
|
||||||
|
"name": "campaign_characters_id_campaign_id_fk",
|
||||||
|
"tableFrom": "campaign_characters",
|
||||||
|
"tableTo": "campaign",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
},
|
||||||
|
"campaign_characters_character_character_id_fk": {
|
||||||
|
"name": "campaign_characters_character_character_id_fk",
|
||||||
|
"tableFrom": "campaign_characters",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"campaign_characters_id_character_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"name": "campaign_characters_id_character_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"campaign_members": {
|
||||||
|
"name": "campaign_members",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rights": {
|
||||||
|
"name": "rights",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"campaign_members_id_campaign_id_fk": {
|
||||||
|
"name": "campaign_members_id_campaign_id_fk",
|
||||||
|
"tableFrom": "campaign_members",
|
||||||
|
"tableTo": "campaign",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
},
|
||||||
|
"campaign_members_user_users_id_fk": {
|
||||||
|
"name": "campaign_members_user_users_id_fk",
|
||||||
|
"tableFrom": "campaign_members",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"campaign_members_id_user_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"name": "campaign_members_id_user_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"campaign": {
|
||||||
|
"name": "campaign",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"campaign_owner_users_id_fk": {
|
||||||
|
"name": "campaign_owner_users_id_fk",
|
||||||
|
"tableFrom": "campaign",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_abilities": {
|
||||||
|
"name": "character_abilities",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ability": {
|
||||||
|
"name": "ability",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"name": "max",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_abilities_character_character_id_fk": {
|
||||||
|
"name": "character_abilities_character_character_id_fk",
|
||||||
|
"tableFrom": "character_abilities",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_abilities_character_ability_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"ability"
|
||||||
|
],
|
||||||
|
"name": "character_abilities_character_ability_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_choices": {
|
||||||
|
"name": "character_choices",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_choices_character_character_id_fk": {
|
||||||
|
"name": "character_choices_character_character_id_fk",
|
||||||
|
"tableFrom": "character_choices",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_choices_character_id_choice_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"id",
|
||||||
|
"choice"
|
||||||
|
],
|
||||||
|
"name": "character_choices_character_id_choice_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_leveling": {
|
||||||
|
"name": "character_leveling",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_leveling_character_character_id_fk": {
|
||||||
|
"name": "character_leveling_character_character_id_fk",
|
||||||
|
"tableFrom": "character_leveling",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_leveling_character_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_leveling_character_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"people": {
|
||||||
|
"name": "people",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"name": "variables",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
|
||||||
|
},
|
||||||
|
"aspect": {
|
||||||
|
"name": "aspect",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"public_notes": {
|
||||||
|
"name": "public_notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"private_notes": {
|
||||||
|
"name": "private_notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'private'"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_owner_users_id_fk": {
|
||||||
|
"name": "character_owner_users_id_fk",
|
||||||
|
"tableFrom": "character",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"character_training": {
|
||||||
|
"name": "character_training",
|
||||||
|
"columns": {
|
||||||
|
"character": {
|
||||||
|
"name": "character",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"stat": {
|
||||||
|
"name": "stat",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"name": "level",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"choice": {
|
||||||
|
"name": "choice",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"character_training_character_character_id_fk": {
|
||||||
|
"name": "character_training_character_character_id_fk",
|
||||||
|
"tableFrom": "character_training",
|
||||||
|
"tableTo": "character",
|
||||||
|
"columnsFrom": [
|
||||||
|
"character"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"character_training_character_stat_level_pk": {
|
||||||
|
"columns": [
|
||||||
|
"character",
|
||||||
|
"stat",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"name": "character_training_character_stat_level_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"project_content": {
|
||||||
|
"name": "project_content",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"project_files": {
|
||||||
|
"name": "project_files",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"navigable": {
|
||||||
|
"name": "navigable",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"project_files_path_unique": {
|
||||||
|
"name": "project_files_path_unique",
|
||||||
|
"columns": [
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"project_files_owner_users_id_fk": {
|
||||||
|
"name": "project_files_owner_users_id_fk",
|
||||||
|
"tableFrom": "project_files",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user_permissions": {
|
||||||
|
"name": "user_permissions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"name": "permission",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_permissions_id_users_id_fk": {
|
||||||
|
"name": "user_permissions_id_users_id_fk",
|
||||||
|
"tableFrom": "user_permissions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_permissions_id_permission_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"name": "user_permissions_id_permission_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user_sessions": {
|
||||||
|
"name": "user_sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_sessions_user_id_users_id_fk": {
|
||||||
|
"name": "user_sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "user_sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_sessions_id_user_id_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"name": "user_sessions_id_user_id_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users_data": {
|
||||||
|
"name": "users_data",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"signin": {
|
||||||
|
"name": "signin",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"users_data_id_users_id_fk": {
|
||||||
|
"name": "users_data_id_users_id_fk",
|
||||||
|
"tableFrom": "users_data",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"name": "hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"users_hash_unique": {
|
||||||
|
"name": "users_hash_unique",
|
||||||
|
"columns": [
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,6 +127,13 @@
|
|||||||
"when": 1760531331328,
|
"when": 1760531331328,
|
||||||
"tag": "0017_workable_scrambler",
|
"tag": "0017_workable_scrambler",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1761829250157,
|
||||||
|
"tag": "0018_friendly_deadpool",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
17
error.vue
17
error.vue
@@ -1,16 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Erreur {{ error?.statusCode }}</Title>
|
<Title>d[any] - Erreur {{ error?.statusCode }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden justify-center items-center flex-col gap-4">
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden justify-center items-center flex-col gap-4">
|
||||||
<NuxtRouteAnnouncer/>
|
<NuxtRouteAnnouncer/>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
<Icon icon="si:error-line" class="w-12 h-12 text-light-60 dark:text-dark-60"/>
|
<Icon icon="si:error-line" class="w-12 h-12 text-light-60 dark:text-dark-60"/>
|
||||||
<div class="text-3xl">Une erreur est survenue.</div>
|
<div class="text-3xl">Une erreur est survenue.</div>
|
||||||
</div>
|
</div>
|
||||||
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
||||||
<Button @click="handleError">Revenir en lieu sûr</Button>
|
<button class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||||
</div>
|
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||||
|
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||||
|
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 p-2" @click="handleError">Revenir en lieu sûr</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -56,48 +56,51 @@ import { Content, iconByType } from '#shared/content.util';
|
|||||||
import { dom, icon } from '#shared/dom.util';
|
import { dom, icon } from '#shared/dom.util';
|
||||||
import { unifySlug } from '#shared/general.util';
|
import { unifySlug } from '#shared/general.util';
|
||||||
import { tooltip } from '#shared/floating.util';
|
import { tooltip } from '#shared/floating.util';
|
||||||
import { link } from '#shared/components.util';
|
import { link, loading } from '#shared/components.util';
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
|
let tree: TreeDOM | undefined;
|
||||||
const { loggedIn, user } = useUserSession();
|
const { loggedIn, user } = useUserSession();
|
||||||
const { fetch } = useContent();
|
|
||||||
|
|
||||||
await fetch(false);
|
|
||||||
|
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
||||||
|
|
||||||
await Content.init();
|
|
||||||
const tree = new TreeDOM((item, depth) => {
|
|
||||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
|
||||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
|
|
||||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
|
||||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
|
||||||
])]);
|
|
||||||
}, (item, depth) => {
|
|
||||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link([
|
|
||||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
|
||||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
|
||||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
|
||||||
], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined )]);
|
|
||||||
}, (item) => item.navigable);
|
|
||||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
|
||||||
const treeParent = useTemplateRef('treeParent');
|
|
||||||
|
|
||||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||||
if(failure)
|
if(failure)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree?.toggle(tree.tree.search('path', e)[0], true));
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const treeParent = useTemplateRef('treeParent');
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if(treeParent.value)
|
if(treeParent.value)
|
||||||
treeParent.value.appendChild(tree.container);
|
{
|
||||||
|
treeParent.value.replaceChildren(loading('normal'));
|
||||||
|
Content.ready.then(() => {
|
||||||
|
tree = new TreeDOM((item, depth) => {
|
||||||
|
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
||||||
|
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
|
||||||
|
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||||
|
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||||
|
])]);
|
||||||
|
}, (item, depth) => {
|
||||||
|
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link([
|
||||||
|
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||||
|
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||||
|
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||||
|
], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined )]);
|
||||||
|
}, (item) => item.navigable);
|
||||||
|
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree?.toggle(tree.tree.search('path', e)[0], true));
|
||||||
|
|
||||||
|
treeParent.value!.replaceChildren(tree.container);
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unmount();
|
unmount();
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ import { hasPermissions } from "#shared/auth.util";
|
|||||||
|
|
||||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
const { loggedIn, fetch, user } = useUserSession();
|
const { loggedIn, fetch, user } = useUserSession();
|
||||||
const { fetch: fetchContent } = useContent();
|
|
||||||
const meta = to.meta;
|
const meta = to.meta;
|
||||||
|
|
||||||
if(await fetch())
|
await fetch()
|
||||||
{
|
|
||||||
fetchContent(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
if(!!meta.guestsGoesTo && !loggedIn.value)
|
||||||
{
|
{
|
||||||
@@ -34,7 +30,7 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||||||
}
|
}
|
||||||
else if(!hasPermissions(user.value.permissions, meta.rights))
|
else if(!hasPermissions(user.value.permissions, meta.rights))
|
||||||
{
|
{
|
||||||
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CharacterBuilder } from '#shared/character.util';
|
import { CharacterBuilder } from '#shared/character.util';
|
||||||
import { unifySlug } from '~/shared/general.util';
|
import { unifySlug } from '#shared/general.util';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
guestsGoesTo: '/user/login',
|
guestsGoesTo: '/user/login',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import characterConfig from '#shared/character-config.json';
|
|||||||
import { unifySlug } from '#shared/general.util';
|
import { unifySlug } from '#shared/general.util';
|
||||||
import type { CharacterConfig } from '~/types/character';
|
import type { CharacterConfig } from '~/types/character';
|
||||||
import { CharacterSheet } from '#shared/character.util';
|
import { CharacterSheet } from '#shared/character.util';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
||||||
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
|
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
|
||||||
@@ -26,7 +27,7 @@ onMounted(() => {
|
|||||||
if(container.value && id)
|
if(container.value && id)
|
||||||
{
|
{
|
||||||
const character = new CharacterSheet(id, user);
|
const character = new CharacterSheet(id, user);
|
||||||
container.value.replaceWith(character.container);
|
container.value.appendChild(character.container);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { HomebrewBuilder } from '~/shared/feature.util';
|
import { HomebrewBuilder } from '#shared/feature.util';
|
||||||
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const route = useRouter().currentRoute;
|
|||||||
const path = computed(() => unifySlug(route.value.params.path ?? ''));
|
const path = computed(() => unifySlug(route.value.params.path ?? ''));
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if(element.value && path.value && await Content.ready)
|
if(element.value && path.value)
|
||||||
{
|
{
|
||||||
overview.value = Content.render(element.value, path.value);
|
overview.value = await Content.render(element.value, path.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -2,31 +2,43 @@
|
|||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Modification</Title>
|
<Title>d[any] - Modification</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden">
|
<div class="flex flex-row w-full max-w-full h-full max-h-full xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3" style="--sidebar-width: 300px">
|
||||||
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
|
<div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
|
||||||
<div class="flex items-center px-2 gap-4">
|
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
|
||||||
<!-- <CollapsibleTrigger asChild>
|
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||||
<Button icon class="!bg-transparent group md:hidden">
|
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||||
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
|
<span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
|
||||||
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
|
</NuxtLink>
|
||||||
</Button>
|
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
|
||||||
</CollapsibleTrigger> -->
|
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
|
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
|
||||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
Copyright Peaceultime - 2025
|
||||||
<span class="text-xl max-md:hidden">d[any]</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center px-2 gap-4">
|
|
||||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
|
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
|
||||||
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
|
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
|
||||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
|
<div class="flex flex-row gap-16 items-center">
|
||||||
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
|
<NavigationMenuRoot class="relative">
|
||||||
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||||
<p>Copyright Peaceultime - 2025</p>
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>
|
||||||
|
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
|
||||||
|
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||||
|
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||||
|
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||||
|
</div>
|
||||||
|
</NavigationMenuRoot>
|
||||||
|
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-16 items-center">
|
||||||
|
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
|
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
|
||||||
@@ -40,6 +52,7 @@ import { button, loading } from '#shared/components.util';
|
|||||||
import { dom, icon } from '#shared/dom.util';
|
import { dom, icon } from '#shared/dom.util';
|
||||||
import { modal, tooltip } from '#shared/floating.util';
|
import { modal, tooltip } from '#shared/floating.util';
|
||||||
import { Toaster } from '#shared/components.util';
|
import { Toaster } from '#shared/components.util';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
rights: ['admin', 'editor'],
|
rights: ['admin', 'editor'],
|
||||||
@@ -73,7 +86,7 @@ function push()
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if(tree.value && container.value && await Content.ready)
|
if(tree.value && container.value)
|
||||||
{
|
{
|
||||||
const load = loading('normal');
|
const load = loading('normal');
|
||||||
tree.value.appendChild(load);
|
tree.value.appendChild(load);
|
||||||
@@ -87,7 +100,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
editor = new Editor();
|
editor = new Editor();
|
||||||
|
|
||||||
tree.value.replaceChild(editor.tree.container, load);
|
Content.ready.then(() => tree.value!.replaceChild(editor.tree.container, load));
|
||||||
container.value.appendChild(editor.container);
|
container.value.appendChild(editor.container);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/login
|
|||||||
ignoreResponseError: true,
|
ignoreResponseError: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const toastMessage = ref('');
|
|
||||||
|
|
||||||
async function submit()
|
async function submit()
|
||||||
{
|
{
|
||||||
if(state.usernameOrEmail === "")
|
if(state.usernameOrEmail === "")
|
||||||
@@ -65,7 +63,7 @@ async function submit()
|
|||||||
{
|
{
|
||||||
Toaster.clear();
|
Toaster.clear();
|
||||||
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||||
await navigateTo('/user/profile');
|
useRouter().push({ name: 'user-profile' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
import { hasPermissions } from '#shared/auth.util';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const session = await getUserSession(e);
|
const session = await getUserSession(e);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { eq, SQL, type Operators } from 'drizzle-orm';
|
import { eq, SQL, type Operators } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable, userPermissionsTable } from '~/db/schema';
|
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
import { hasPermissions } from '#shared/auth.util';
|
||||||
import { group } from '~/shared/general.util';
|
import { group } from '#shared/general.util';
|
||||||
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable } from '~/db/schema';
|
import { characterTable } from '~/db/schema';
|
||||||
import { group } from '~/shared/general.util';
|
import { group } from '#shared/general.util';
|
||||||
import type { Character, CharacterVariables, Level, MainStat, TrainingLevel } from '~/types/character';
|
import type { Character, CharacterVariables, Level, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable } from '~/db/schema';
|
import { characterTable } from '~/db/schema';
|
||||||
import { CharacterVariablesValidation } from '~/shared/character.util';
|
import { CharacterVariablesValidation } from '#shared/character.util';
|
||||||
import type { CharacterVariables } from '~/types/character';
|
import type { CharacterVariables } from '~/types/character';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ export default defineEventHandler(async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(id);
|
|
||||||
|
|
||||||
setResponseStatus(e, 200);
|
setResponseStatus(e, 200);
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
@@ -15,9 +15,6 @@ export default defineEventHandler(async (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(id);
|
|
||||||
console.log(await readBody(e));
|
|
||||||
|
|
||||||
setResponseStatus(e, 200);
|
setResponseStatus(e, 200);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import Bun from 'bun';
|
import Bun from 'bun';
|
||||||
import { format } from '~/shared/general.util';
|
import { format } from '#shared/general.util';
|
||||||
|
|
||||||
const { id, userId, username, timestamp } = defineProps<{
|
const { id, userId, username, timestamp } = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default defineTask({
|
|||||||
function reshapeLinks(content: string | null, all: ProjectContent[])
|
function reshapeLinks(content: string | null, all: ProjectContent[])
|
||||||
{
|
{
|
||||||
return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => {
|
return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => {
|
||||||
return `[[${link ? parsePath(all.find(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`;
|
return `[[${link ? parsePath(all.findLast(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -482,8 +482,6 @@ export class Canvas
|
|||||||
]),
|
]),
|
||||||
]), this.transform,
|
]), this.transform,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(this.nodes.length, this.edges.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected computeLimits()
|
protected computeLimits()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,15 +1,15 @@
|
|||||||
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponType } from "~/types/character";
|
import type { Ability, Alignment, ArmorConfig, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, ItemConfig, ItemState, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponType } from "~/types/character";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import characterConfig from '#shared/character-config.json';
|
import characterConfig from '#shared/character-config.json';
|
||||||
import proses, { preview } from "#shared/proses";
|
import proses, { preview } from "#shared/proses";
|
||||||
import { button, buttongroup, floater, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
||||||
import { div, dom, icon, span, text } from "#shared/dom.util";
|
import { div, dom, icon, span, text } from "#shared/dom.util";
|
||||||
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||||
import { clamp } from "#shared/general.util";
|
import { clamp } from "#shared/general.util";
|
||||||
import markdown from "#shared/markdown.util";
|
import markdown from "#shared/markdown.util";
|
||||||
import { getText } from "./i18n";
|
import { getText } from "#shared/i18n";
|
||||||
import type { User } from "~/types/auth";
|
import type { User } from "~/types/auth";
|
||||||
import { MarkdownEditor } from "./editor.util";
|
import { MarkdownEditor } from "#shared/editor.util";
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
|
|
||||||
@@ -134,7 +134,6 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|||||||
aspect: "",
|
aspect: "",
|
||||||
notes: Object.assign({ public: '', private: '' }, character.notes),
|
notes: Object.assign({ public: '', private: '' }, character.notes),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mainStatTexts: Record<MainStat, string> = {
|
export const mainStatTexts: Record<MainStat, string> = {
|
||||||
"strength": "Force",
|
"strength": "Force",
|
||||||
"dexterity": "Dextérité",
|
"dexterity": "Dextérité",
|
||||||
@@ -153,7 +152,6 @@ export const mainStatShortTexts: Record<MainStat, string> = {
|
|||||||
"charisma": "CHA",
|
"charisma": "CHA",
|
||||||
"psyche": "PSY",
|
"psyche": "PSY",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
|
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
|
||||||
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
|
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
|
||||||
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
|
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
|
||||||
@@ -169,7 +167,6 @@ export const elementDom = (element: SpellElement) => dom("span", {
|
|||||||
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class],
|
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class],
|
||||||
text: elementTexts[element].text
|
text: elementTexts[element].text
|
||||||
});
|
});
|
||||||
|
|
||||||
export const alignmentTexts: Record<Alignment, string> = {
|
export const alignmentTexts: Record<Alignment, string> = {
|
||||||
'loyal_good': 'Loyal bon',
|
'loyal_good': 'Loyal bon',
|
||||||
'neutral_good': 'Neutre bon',
|
'neutral_good': 'Neutre bon',
|
||||||
@@ -182,7 +179,6 @@ export const alignmentTexts: Record<Alignment, string> = {
|
|||||||
'chaotic_evil': 'Chaotique mauvais',
|
'chaotic_evil': 'Chaotique mauvais',
|
||||||
};
|
};
|
||||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||||
|
|
||||||
export const abilityTexts: Record<Ability, string> = {
|
export const abilityTexts: Record<Ability, string> = {
|
||||||
"athletics": "Athlétisme",
|
"athletics": "Athlétisme",
|
||||||
"acrobatics": "Acrobatique",
|
"acrobatics": "Acrobatique",
|
||||||
@@ -202,7 +198,6 @@ export const abilityTexts: Record<Ability, string> = {
|
|||||||
"animalhandling": "Dressage",
|
"animalhandling": "Dressage",
|
||||||
"deception": "Mensonge"
|
"deception": "Mensonge"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resistanceTexts: Record<Resistance, string> = {
|
export const resistanceTexts: Record<Resistance, string> = {
|
||||||
'stun': 'Hébètement',
|
'stun': 'Hébètement',
|
||||||
'bleed': 'Saignement',
|
'bleed': 'Saignement',
|
||||||
@@ -224,23 +219,19 @@ export const damageTypeTexts: Record<DamageType, string> = {
|
|||||||
'slashing': 'Tranchant',
|
'slashing': 'Tranchant',
|
||||||
'thunder': 'Foudre',
|
'thunder': 'Foudre',
|
||||||
};
|
};
|
||||||
export const weaponTypeTexts: Record<WeaponType, string> = {
|
|
||||||
"light": "Arme légère",
|
|
||||||
"shield": "Bouclier",
|
|
||||||
"heavy": "Arme lourde",
|
|
||||||
"classic": "Arme",
|
|
||||||
"throw": "Arme de jet",
|
|
||||||
"natural": "Arme naturelle",
|
|
||||||
"twohanded": "Deux mains",
|
|
||||||
"finesse": "Arme maniable",
|
|
||||||
"reach": "Arme longue",
|
|
||||||
"projectile": "Arme à projectile",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CharacterNotesValidation = z.object({
|
export const CharacterNotesValidation = z.object({
|
||||||
public: z.string().optional(),
|
public: z.string().optional(),
|
||||||
private: z.string().optional(),
|
private: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
export const ItemStateValidation = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
amount: z.number().min(1),
|
||||||
|
enchantments: z.array(z.string()).optional(),
|
||||||
|
charges: z.number().optional(),
|
||||||
|
equipped: z.boolean().optional(),
|
||||||
|
state: z.any().optional(),
|
||||||
|
})
|
||||||
export const CharacterVariablesValidation = z.object({
|
export const CharacterVariablesValidation = z.object({
|
||||||
health: z.number(),
|
health: z.number(),
|
||||||
mana: z.number(),
|
mana: z.number(),
|
||||||
@@ -255,7 +246,9 @@ export const CharacterVariablesValidation = z.object({
|
|||||||
state: z.number().min(1).max(7).or(z.literal(true)),
|
state: z.number().min(1).max(7).or(z.literal(true)),
|
||||||
})),
|
})),
|
||||||
spells: z.array(z.string()),
|
spells: z.array(z.string()),
|
||||||
items: z.array(z.string()),
|
items: z.array(ItemStateValidation),
|
||||||
|
|
||||||
|
money: z.number(),
|
||||||
});
|
});
|
||||||
export const CharacterValidation = z.object({
|
export const CharacterValidation = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@@ -1229,6 +1222,65 @@ class AspectPicker extends BuilderTab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Category = ItemConfig['category'];
|
||||||
|
type Rarity = ItemConfig['rarity'];
|
||||||
|
export const colorByRarity: Record<Rarity, string> = {
|
||||||
|
'common': 'text-light-100 dark:text-dark-100',
|
||||||
|
'uncommon': 'text-light-cyan dark:text-dark-cyan',
|
||||||
|
'rare': 'text-light-purple dark:text-dark-purple',
|
||||||
|
'legendary': 'text-light-orange dark:text-dark-orange'
|
||||||
|
}
|
||||||
|
export const weaponTypeTexts: Record<WeaponType, string> = {
|
||||||
|
"light": 'légère',
|
||||||
|
"shield": 'bouclier',
|
||||||
|
"heavy": 'lourde',
|
||||||
|
"classic": 'arme',
|
||||||
|
"throw": 'de jet',
|
||||||
|
"natural": 'naturelle',
|
||||||
|
"twohanded": 'à deux mains',
|
||||||
|
"finesse": 'maniable',
|
||||||
|
"reach": 'longue',
|
||||||
|
"projectile": 'à projectile',
|
||||||
|
}
|
||||||
|
export const armorTypeTexts: Record<ArmorConfig["type"], string> = {
|
||||||
|
'heavy': 'Armure lourde',
|
||||||
|
'light': 'Armure légère',
|
||||||
|
'medium': 'Armure',
|
||||||
|
}
|
||||||
|
export const categoryText: Record<Category, string> = {
|
||||||
|
'mundane': 'Objet',
|
||||||
|
'armor': 'Armure',
|
||||||
|
'weapon': 'Arme',
|
||||||
|
'wondrous': 'Objet magique'
|
||||||
|
};
|
||||||
|
export const rarityText: Record<Rarity, string> = {
|
||||||
|
'common': 'Commun',
|
||||||
|
'uncommon': 'Atypique',
|
||||||
|
'rare': 'Rare',
|
||||||
|
'legendary': 'Légendaire'
|
||||||
|
};
|
||||||
|
const subnameFactory = (item: ItemConfig, state?: ItemState): string[] => {
|
||||||
|
let result = [];
|
||||||
|
switch(item.category)
|
||||||
|
{
|
||||||
|
case 'armor':
|
||||||
|
result = [armorTypeTexts[(item as ArmorConfig).type]];
|
||||||
|
break;
|
||||||
|
case 'weapon':
|
||||||
|
result = ['Arme', ...(item as WeaponConfig).type.filter(e => e !== 'classic').map(e => weaponTypeTexts[e])];
|
||||||
|
break;
|
||||||
|
case 'mundane':
|
||||||
|
result = ['Objet'];
|
||||||
|
break;
|
||||||
|
case 'wondrous':
|
||||||
|
result = ['Objet magique'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(state && state.enchantments !== undefined && state.enchantments.length > 0) result.push('Enchanté');
|
||||||
|
if(item.consummable) result.push('Consommable');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
export class CharacterSheet
|
export class CharacterSheet
|
||||||
{
|
{
|
||||||
user: ComputedRef<User | null>;
|
user: ComputedRef<User | null>;
|
||||||
@@ -1283,6 +1335,38 @@ export class CharacterSheet
|
|||||||
publicNotes.content = this.character!.character.notes!.public!;
|
publicNotes.content = this.character!.character.notes!.public!;
|
||||||
privateNotes.content = this.character!.character.notes!.private!;
|
privateNotes.content = this.character!.character.notes!.private!;
|
||||||
|
|
||||||
|
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: HTMLElement }) => {
|
||||||
|
const value = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10);
|
||||||
|
this.character?.variable(property, clamp(isNaN(value) ? character.variables[property] : value, 0, Infinity));
|
||||||
|
this.character?.saveVariables();
|
||||||
|
|
||||||
|
obj.edit.value = (character[property] - this.character!.character.variables[property]).toString();
|
||||||
|
obj.readonly.textContent = (character[property] - character.variables[property]).toString();
|
||||||
|
obj.edit.replaceWith(obj.readonly);
|
||||||
|
};
|
||||||
|
|
||||||
|
const health = {
|
||||||
|
readonly: dom("span", {
|
||||||
|
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
||||||
|
text: `${character.health - character.variables.health}`,
|
||||||
|
listeners: { click: () => health.readonly.replaceWith(health.edit) },
|
||||||
|
}),
|
||||||
|
edit: input('text', { defaultValue: (character.health - character.variables.health).toString(), input: (v) => {
|
||||||
|
return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10));
|
||||||
|
}, change: (v) => validateProperty(v, 'health', health), blur: () => validateProperty(health.edit.value, 'health', health), class: 'font-bold px-2 w-20 text-center' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mana = {
|
||||||
|
readonly: dom("span", {
|
||||||
|
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
||||||
|
text: `${character.mana - character.variables.mana}`,
|
||||||
|
listeners: { click: () => mana.readonly.replaceWith(mana.edit) },
|
||||||
|
}),
|
||||||
|
edit: input('text', { defaultValue: (character.mana - character.variables.mana).toString(), input: (v) => {
|
||||||
|
return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10));
|
||||||
|
}, change: (v) => validateProperty(v, 'mana', mana), blur: () => validateProperty(mana.edit.value, 'mana', mana), class: 'font-bold px-2 w-20 text-center' }),
|
||||||
|
};
|
||||||
|
|
||||||
this.tabs = tabgroup([
|
this.tabs = tabgroup([
|
||||||
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
|
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
|
||||||
|
|
||||||
@@ -1290,9 +1374,7 @@ export class CharacterSheet
|
|||||||
|
|
||||||
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
|
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
|
||||||
|
|
||||||
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
{ id: 'inventory', title: [ text('Inventaire') ], content: () => this.itemsTab(character) },
|
||||||
|
|
||||||
] },
|
|
||||||
|
|
||||||
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
@@ -1327,18 +1409,12 @@ export class CharacterSheet
|
|||||||
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
|
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
|
||||||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||||
text("PV: "),
|
text("PV: "),
|
||||||
dom("span", {
|
health.readonly,
|
||||||
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
|
||||||
text: `${character.health - character.variables.health}`
|
|
||||||
}),
|
|
||||||
text(`/ ${character.health}`)
|
text(`/ ${character.health}`)
|
||||||
]),
|
]),
|
||||||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||||
text("Mana: "),
|
text("Mana: "),
|
||||||
dom("span", {
|
mana.readonly,
|
||||||
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
|
|
||||||
text: `${character.mana - character.variables.mana}`
|
|
||||||
}),
|
|
||||||
text(`/ ${character.mana}`)
|
text(`/ ${character.mana}`)
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
@@ -1488,7 +1564,7 @@ export class CharacterSheet
|
|||||||
div('flex flex-col gap-8', [
|
div('flex flex-col gap-8', [
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div("flex flex-row items-center justify-center gap-4", [
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
|
||||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||||
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
||||||
]),
|
]),
|
||||||
@@ -1503,7 +1579,7 @@ export class CharacterSheet
|
|||||||
]),
|
]),
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div("flex flex-row items-center justify-center gap-4", [
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
|
||||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||||
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
||||||
]),
|
]),
|
||||||
@@ -1518,7 +1594,7 @@ export class CharacterSheet
|
|||||||
]),
|
]),
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div("flex flex-row items-center justify-center gap-4", [
|
div("flex flex-row items-center justify-center gap-4", [
|
||||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
|
||||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@@ -1563,7 +1639,7 @@ export class CharacterSheet
|
|||||||
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]),
|
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]),
|
||||||
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
|
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
|
||||||
div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]),
|
div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]),
|
||||||
div('flex flex-row gap-2', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
|
div('flex flex-row gap-4 items-center', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.range === 'number' && e.spell.range > 0 ? `${e.spell.range} case${e.spell.range > 1 ? 's' : ''}` : e.spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
|
||||||
]),
|
]),
|
||||||
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]),
|
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.description) ]),
|
||||||
]) : undefined }));
|
]) : undefined }));
|
||||||
@@ -1576,14 +1652,14 @@ export class CharacterSheet
|
|||||||
]),
|
]),
|
||||||
div('flex flex-row gap-2 items-center', [
|
div('flex flex-row gap-2 items-center', [
|
||||||
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
|
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
|
||||||
button(text('Modifier'), () => this.spellPanel(character, spells), 'py-1 px-4'),
|
button(text('Modifier'), () => this.spellPanel(character), 'py-1 px-4'),
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
div('flex flex-col gap-2', spells.map(e => e.dom))
|
div('flex flex-col gap-2', spells.map(e => e.dom))
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>)
|
spellPanel(character: CompiledCharacter)
|
||||||
{
|
{
|
||||||
const availableSpells = Object.values(config.spells).filter(spell => {
|
const availableSpells = Object.values(config.spells).filter(spell => {
|
||||||
if (spell.rank === 4) return false;
|
if (spell.rank === 4) return false;
|
||||||
@@ -1650,4 +1726,113 @@ export class CharacterSheet
|
|||||||
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
||||||
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
||||||
}
|
}
|
||||||
|
itemsTab(character: CompiledCharacter)
|
||||||
|
{
|
||||||
|
let debounceId: NodeJS.Timeout | undefined;
|
||||||
|
//TODO: Recompile values on "equip" checkbox change
|
||||||
|
const items = (character.variables.items.map(e => ({ ...e, item: config.items[e.id] })).filter(e => !!e.item) as Array<ItemState & { item: ItemConfig }>).map(e => div('flex flex-row justify-between', [
|
||||||
|
div('flex flex-row items-center gap-4', [
|
||||||
|
div('flex flex-col gap-1', [ e.item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
|
||||||
|
e.equipped = v;
|
||||||
|
|
||||||
|
this.character!.variable('items', this.character!.character.variables.items);
|
||||||
|
|
||||||
|
debounceId && clearTimeout(debounceId);
|
||||||
|
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
|
||||||
|
|
||||||
|
this.tabs?.refresh();
|
||||||
|
}, class: { container: '!w-5 !h-5' } }) : checkbox({ disabled: true, class: { container: '!w-5 !h-5' } }), button(icon('radix-icons:trash', { width: 16, height: 17 }), () => {
|
||||||
|
const idx = this.character!.character.variables.items.findIndex(_e => _e.id === e.id);
|
||||||
|
if(idx === -1) return;
|
||||||
|
|
||||||
|
this.character!.character.variables.items[idx]!.amount--;
|
||||||
|
if(this.character!.character.variables.items[idx]!.amount >= 0) this.character!.character.variables.items.splice(idx, 1);
|
||||||
|
|
||||||
|
this.character!.variable('items', this.character!.character.variables.items);
|
||||||
|
|
||||||
|
debounceId && clearTimeout(debounceId);
|
||||||
|
debounceId = setTimeout(() => this.character?.saveVariables(), 2000);
|
||||||
|
|
||||||
|
this.tabs?.refresh();
|
||||||
|
}, 'p-px') ]),
|
||||||
|
div('flex flex-col gap-1', [ span([colorByRarity[e.item.rarity], 'text-lg'], e.item.name), div('flex flex-row gap-4 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(e.item, e).map(text)) ]),
|
||||||
|
]),
|
||||||
|
div('grid grid-cols-2 row-gap-2 col-gap-8', [
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', (e.item.powercost || (e.enchantments && e.enchantments.length > 0)) && e.item.capacity ? `${(e.item?.powercost ?? 0) + (e.enchantments?.reduce((p, v) => (config.enchantments[v]?.power ?? 0) + p, 0) ?? 0)}/${e.item.capacity}` : '-') ]),
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', e.item.weight?.toString() ?? '-') ]),
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', e.charges && e.item.charge ? `${e.charges}/${e.item.charge}` : '-') ]),
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('flex-1', e.amount?.toString() ?? '-') ])
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
|
||||||
|
const power = character.variables.items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + (config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0), 0);
|
||||||
|
const weight = character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0), 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
div('flex flex-col gap-2', [
|
||||||
|
div('flex flex-row justify-end items-center gap-8', [
|
||||||
|
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': weight > character.itempower }], text: `Poids total: ${weight}/${character.itempower}` }),
|
||||||
|
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': power > (character.capacity === false ? 0 : character.capacity) }], text: `Puissance magique: ${power}/${character.capacity}` }),
|
||||||
|
button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'),
|
||||||
|
]),
|
||||||
|
div('grid grid-cols-2 flex-1 gap-4', items)
|
||||||
|
])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
itemsPanel(character: CompiledCharacter)
|
||||||
|
{
|
||||||
|
const items = Object.values(config.items).map(item => ({ item, dom: foldable(() => [ markdown(getText(item.description)) ], [div('flex flex-row justify-between', [
|
||||||
|
div('flex flex-row items-center gap-4', [
|
||||||
|
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
|
||||||
|
]),
|
||||||
|
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]),
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.weight?.toString() ?? '-') ]),
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.charge ? `${item.charge}` : '-') ]),
|
||||||
|
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]),
|
||||||
|
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
|
||||||
|
const list = [...this.character!.character.variables.items];
|
||||||
|
if(item.equippable) list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false });
|
||||||
|
else if(list.find(e => e.id === item.id)) this.character!.character.variables.items.find(e => e.id === item.id)!.amount++;
|
||||||
|
else list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] });
|
||||||
|
this.character!.variable('items', list); //TO REWORK
|
||||||
|
this.tabs?.refresh();
|
||||||
|
}, 'p-1 !border-solid !border-r'),
|
||||||
|
]),
|
||||||
|
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } }) }));
|
||||||
|
|
||||||
|
const filters: { category: Category[], rarity: Rarity[], name: string, power: { min: number, max: number } } = {
|
||||||
|
category: [],
|
||||||
|
rarity: [],
|
||||||
|
name: '',
|
||||||
|
power: { min: 0, max: Infinity },
|
||||||
|
};
|
||||||
|
const applyFilters = () => {
|
||||||
|
content.replaceChildren(...items.filter(e =>
|
||||||
|
(filters.category.length === 0 || filters.category.includes(e.item.category)) &&
|
||||||
|
(filters.rarity.length === 0 || filters.rarity.includes(e.item.rarity)) &&
|
||||||
|
(filters.name === '' || e.item.name.toLowerCase().includes(filters.name.toLowerCase()))
|
||||||
|
).map(e => e.dom));
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = div('grid grid-cols-1 -my-2 overflow-y-auto gap-1');
|
||||||
|
const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
|
||||||
|
div("flex flex-row justify-between items-center mb-4", [
|
||||||
|
dom("h2", { class: "text-xl font-bold", text: "Gestion de l'inventaire" }),
|
||||||
|
div('flex flex-row gap-4 items-center', [ tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
|
||||||
|
setTimeout(blocker.close, 150);
|
||||||
|
container.setAttribute('data-state', 'inactive');
|
||||||
|
}, "p-1"), "Fermer", "left") ])
|
||||||
|
]),
|
||||||
|
div('flex flex-row items-center gap-4', [
|
||||||
|
div('flex flex-row gap-2 items-center', [ text('Catégorie'), multiselect(Object.keys(categoryText).map(e => ({ text: categoryText[e as Category], value: e as Category })), { defaultValue: filters.category, change: v => { filters.category = v; applyFilters(); }, class: { container: 'w-40' } }) ]),
|
||||||
|
div('flex flex-row gap-2 items-center', [ text('Rareté'), multiselect(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: filters.rarity, change: v => { filters.rarity = v; applyFilters(); }, class: { container: 'w-40' } }) ]),
|
||||||
|
div('flex flex-row gap-2 items-center', [ text('Nom'), input('text', { defaultValue: filters.name, input: v => { filters.name = v; applyFilters(); }, class: 'w-64' }) ]),
|
||||||
|
]),
|
||||||
|
content,
|
||||||
|
]);
|
||||||
|
applyFilters();
|
||||||
|
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
||||||
|
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
||||||
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
|
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
|
||||||
import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util";
|
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "./floating.util";
|
||||||
import { clamp } from "./general.util";
|
import { clamp } from "./general.util";
|
||||||
import { Tree } from "./tree";
|
import { Tree } from "./tree";
|
||||||
import type { Placement } from "@floating-ui/dom";
|
import type { Placement } from "@floating-ui/dom";
|
||||||
@@ -368,17 +368,18 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
|||||||
})
|
})
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
|
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void | boolean, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
|
||||||
{
|
{
|
||||||
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, class: [`mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, class: [`mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||||
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
|
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
|
||||||
input: () => settings?.input && settings.input(input.value),
|
input: (e) => { if(settings?.input && settings.input(input.value) === false) input.value = value; else value = input.value; },
|
||||||
change: () => settings?.change && settings.change(input.value),
|
change: () => settings?.change && settings.change(input.value),
|
||||||
focus: () => settings?.focus,
|
focus: settings?.focus,
|
||||||
blur: () => settings?.blur,
|
blur: settings?.blur,
|
||||||
}})
|
}})
|
||||||
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
||||||
|
let value = input.value;
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
@@ -481,7 +482,7 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HT
|
|||||||
let state = settings?.defaultValue ?? false;
|
let state = settings?.defaultValue ?? false;
|
||||||
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
|
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
|
||||||
cursor-pointer hover:bg-light-30 dark:hover:bg-dark-30 hover:border-light-60 dark:hover:border-dark-60
|
cursor-pointer hover:bg-light-30 dark:hover:bg-dark-30 hover:border-light-60 dark:hover:border-dark-60
|
||||||
data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
|
data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0 hover:data-[disabled]:bg-0 dark:hover:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
|
||||||
click: function(e: Event) {
|
click: function(e: Event) {
|
||||||
if(this.hasAttribute('data-disabled'))
|
if(this.hasAttribute('data-disabled'))
|
||||||
return;
|
return;
|
||||||
@@ -526,16 +527,16 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
|
|||||||
})
|
})
|
||||||
return container as HTMLDivElement & { refresh: () => void };
|
return container as HTMLDivElement & { refresh: () => void };
|
||||||
}
|
}
|
||||||
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
|
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
|
||||||
{
|
{
|
||||||
let viewport = document.getElementById('mainContainer') ?? undefined;
|
let viewport = document.getElementById('mainContainer') ?? undefined;
|
||||||
let diffX, diffY;
|
let diffX, diffY;
|
||||||
let minimizeBox: DOMRect, minimized = false;
|
let minimizeRect: DOMRect, minimized = false;
|
||||||
|
|
||||||
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
|
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
|
||||||
show: ['mouseenter', 'mousemove', 'focus'],
|
show: ['mouseenter', 'mousemove', 'focus'],
|
||||||
hide: ['mouseleave', 'blur'],
|
hide: ['mouseleave', 'blur'],
|
||||||
}, settings?.events ?? {});
|
} as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
|
||||||
|
|
||||||
if(settings?.pinned)
|
if(settings?.pinned)
|
||||||
{
|
{
|
||||||
@@ -631,23 +632,30 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
|||||||
floating.content.toggleAttribute('data-minimized', minimized);
|
floating.content.toggleAttribute('data-minimized', minimized);
|
||||||
if(minimized)
|
if(minimized)
|
||||||
{
|
{
|
||||||
minimizeBox = floating.content.getBoundingClientRect();
|
minimizeRect = floating.content.getBoundingClientRect();
|
||||||
Object.assign(floating.content.style, {
|
Object.assign(floating.content.style, {
|
||||||
left: `0px`,
|
|
||||||
top: `initial`,
|
|
||||||
bottom: `0px`,
|
|
||||||
width: `150px`,
|
width: `150px`,
|
||||||
height: `21px`,
|
height: `21px`,
|
||||||
|
position: 'initial',
|
||||||
});
|
});
|
||||||
|
floating.content.style.setProperty('top', null);
|
||||||
|
floating.content.style.setProperty('left', null);
|
||||||
|
floating.content.style.setProperty('bottom', null);
|
||||||
|
floating.content.style.setProperty('right', null);
|
||||||
|
|
||||||
|
minimizeBox.appendChild(floating.content);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Object.assign(floating.content.style, {
|
Object.assign(floating.content.style, {
|
||||||
left: `${minimizeBox.left}px`,
|
left: `${minimizeRect.left}px`,
|
||||||
top: `${minimizeBox.top}px`,
|
top: `${minimizeRect.top}px`,
|
||||||
width: `${minimizeBox.width}px`,
|
width: `${minimizeRect.width}px`,
|
||||||
height: `${minimizeBox.height}px`,
|
height: `${minimizeRect.height}px`,
|
||||||
});
|
});
|
||||||
|
floating.content.style.setProperty('position', null);
|
||||||
|
|
||||||
|
teleport.appendChild(floating.content);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -657,10 +665,11 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
|||||||
offset: 12,
|
offset: 12,
|
||||||
cover: settings?.cover,
|
cover: settings?.cover,
|
||||||
placement: settings?.position,
|
placement: settings?.position,
|
||||||
|
style: settings?.style,
|
||||||
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
|
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
|
||||||
content: () => [
|
content: () => [
|
||||||
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [
|
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [
|
||||||
dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
|
dom('span', { class: 'flex-1 w-full h-5 cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
|
||||||
settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined,
|
settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined,
|
||||||
settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined,
|
settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined,
|
||||||
tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
|
tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
|
||||||
@@ -669,10 +678,10 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
|
|||||||
|
|
||||||
floating.content.toggleAttribute('data-minimized', false);
|
floating.content.toggleAttribute('data-minimized', false);
|
||||||
minimized && Object.assign(floating.content.style, {
|
minimized && Object.assign(floating.content.style, {
|
||||||
left: `${minimizeBox.left}px`,
|
left: `${minimizeRect.left}px`,
|
||||||
top: `${minimizeBox.top}px`,
|
top: `${minimizeRect.top}px`,
|
||||||
width: `${minimizeBox.width}px`,
|
width: `${minimizeRect.width}px`,
|
||||||
height: `${minimizeBox.height}px`,
|
height: `${minimizeRect.height}px`,
|
||||||
});
|
});
|
||||||
minimized = false;
|
minimized = false;
|
||||||
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Canvas, CanvasEditor } from "#shared/canvas.util";
|
|||||||
import render from "#shared/markdown.util";
|
import render from "#shared/markdown.util";
|
||||||
import { confirm, contextmenu, tooltip } from "#shared/floating.util";
|
import { confirm, contextmenu, tooltip } from "#shared/floating.util";
|
||||||
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
|
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
|
||||||
import { loading } from "#shared/components.util";
|
import { async, loading } from "#shared/components.util";
|
||||||
import prose, { h1, h2 } from "#shared/proses";
|
import prose, { h1, h2 } from "#shared/proses";
|
||||||
import { getID, parsePath } from '#shared/general.util';
|
import { getID, parsePath } from '#shared/general.util';
|
||||||
import { TreeDOM, type Recursive } from '#shared/tree';
|
import { TreeDOM, type Recursive } from '#shared/tree';
|
||||||
@@ -139,11 +139,12 @@ export class Content
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
|
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
|
||||||
|
await Content.pull();
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
{
|
{
|
||||||
Content._overview = {};
|
Content._overview = {};
|
||||||
await Content.pull();
|
await Content.pull(true);
|
||||||
}
|
}
|
||||||
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => {
|
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => {
|
||||||
p[v.path] = v.id;
|
p[v.path] = v.id;
|
||||||
@@ -230,7 +231,7 @@ export class Content
|
|||||||
|
|
||||||
return Content.queue.promise;
|
return Content.queue.promise;
|
||||||
}
|
}
|
||||||
static async pull()
|
static async pull(force: boolean = false)
|
||||||
{
|
{
|
||||||
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined;
|
const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined;
|
||||||
|
|
||||||
@@ -241,19 +242,16 @@ export class Content
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deletable = Object.keys(Content._overview);
|
||||||
for(const file of overview)
|
for(const file of overview)
|
||||||
{
|
{
|
||||||
const _overview = Content._overview[file.id];
|
const _overview = Content._overview[file.id];
|
||||||
if(_overview && _overview.localEdit)
|
if(force || !_overview || new Date(_overview.timestamp) < new Date(file.timestamp))
|
||||||
{
|
|
||||||
//TODO: Ask what to do about this file.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Content._overview[file.id] = file;
|
Content._overview[file.id] = file;
|
||||||
|
|
||||||
Content.queue.queue(() => {
|
Content.queue.queue(() => {
|
||||||
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined) => {
|
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined | null) => {
|
||||||
if(content)
|
if(content)
|
||||||
{
|
{
|
||||||
if(file.type !== 'folder')
|
if(file.type !== 'folder')
|
||||||
@@ -269,8 +267,13 @@ export class Content
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deletable.splice(deletable.findIndex(e => e === file.id), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(const id of deletable)
|
||||||
|
Content.queue.queue(() => Content.remove(id).then(e => delete Content._overview[id]));
|
||||||
|
|
||||||
return Content.queue.queue(() => {
|
return Content.queue.queue(() => {
|
||||||
return Content.write('overview', JSON.stringify(Content._overview), { create: true });
|
return Content.write('overview', JSON.stringify(Content._overview), { create: true });
|
||||||
});
|
});
|
||||||
@@ -296,18 +299,16 @@ export class Content
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
console.time(`Reading '${path}'`);
|
|
||||||
const handle = await Content.root.getFileHandle(path, options);
|
const handle = await Content.root.getFileHandle(path, options);
|
||||||
const file = await handle.getFile();
|
const file = await handle.getFile();
|
||||||
|
|
||||||
const text = await file.text();
|
//@ts-ignore
|
||||||
console.timeEnd(`Reading '${path}'`);
|
const response = await new Response(file.stream().pipeThrough(new DecompressionStream('gzip')));
|
||||||
return text;
|
return await response.text();
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
{
|
{
|
||||||
console.error(path, e);
|
console.error(path, e);
|
||||||
console.timeEnd(`Reading '${path}'`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static async goto(path: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle | undefined>
|
private static async goto(path: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle | undefined>
|
||||||
@@ -330,22 +331,35 @@ export class Content
|
|||||||
//Easy to use, but not very performant.
|
//Easy to use, but not very performant.
|
||||||
private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void>
|
private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void>
|
||||||
{
|
{
|
||||||
const size = new TextEncoder().encode(content).byteLength;
|
|
||||||
console.time(`Writing ${size} bytes to '${path}'`);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
|
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
|
||||||
const handle = await (await Content.goto(parent, { create: true }) ?? Content.root).getFileHandle(basename, options);
|
const handle = await (await Content.goto(parent, { create: true }) ?? Content.root).getFileHandle(basename, options);
|
||||||
const file = await handle.createWritable({ keepExistingData: false });
|
const file = await handle.createWritable({ keepExistingData: false });
|
||||||
|
|
||||||
await file.write(content);
|
await new ReadableStream({
|
||||||
await file.close();
|
start(controller) {
|
||||||
|
controller.enqueue(new TextEncoder().encode(content));
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}).pipeThrough(new CompressionStream("gzip")).pipeTo(file);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.error(path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static async remove(path: string): Promise<void>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
|
||||||
|
return (await Content.goto(parent, { create: true }) ?? Content.root).removeEntry(basename);
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
{
|
{
|
||||||
console.error(path, e);
|
console.error(path, e);
|
||||||
}
|
}
|
||||||
console.timeEnd(`Writing ${size} bytes to '${path}'`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get estimate(): Promise<StorageEstimate>
|
static get estimate(): Promise<StorageEstimate>
|
||||||
@@ -363,26 +377,27 @@ export class Content
|
|||||||
return handlers[overview.type].fromString(content);
|
return handlers[overview.type].fromString(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
static render(parent: HTMLElement, path: string): Omit<LocalContent, 'content'> | undefined
|
static async render(parent: HTMLElement, path: string): Promise<Omit<LocalContent, 'content'> | undefined>
|
||||||
{
|
{
|
||||||
|
parent.appendChild(dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]))
|
||||||
|
|
||||||
|
await Content.ready;
|
||||||
|
|
||||||
const overview = Content.getFromPath(path);
|
const overview = Content.getFromPath(path);
|
||||||
|
|
||||||
if(!!overview)
|
if(!!overview)
|
||||||
{
|
{
|
||||||
const load = dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]);
|
|
||||||
parent.appendChild(load);
|
|
||||||
|
|
||||||
function _render<T extends FileType>(content: LocalContent<T>): void
|
function _render<T extends FileType>(content: LocalContent<T>): void
|
||||||
{
|
{
|
||||||
const el = handlers[content.type].render(content);
|
const el = handlers[content.type].render(content);
|
||||||
el && parent.replaceChild(el, load);
|
el && parent.replaceChildren(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
Content.getContent(overview.id).then(content => _render(content!));
|
Content.getContent(overview.id).then(content => _render(content!));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
parent.appendChild(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" }));
|
parent.replaceChildren(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return overview;
|
return overview;
|
||||||
@@ -456,7 +471,9 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
|||||||
'cyan': '5',
|
'cyan': '5',
|
||||||
'purple': '6',
|
'purple': '6',
|
||||||
};
|
};
|
||||||
|
//@ts-ignore
|
||||||
content.edges?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
|
content.edges?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
|
||||||
|
//@ts-ignore
|
||||||
content.nodes?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
|
content.nodes?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
|
||||||
|
|
||||||
return JSON.stringify(content);
|
return JSON.stringify(content);
|
||||||
@@ -502,7 +519,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
|||||||
//TODO: Edition link
|
//TODO: Edition link
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
render(content.content),
|
render(content.content, undefined, { class: 'pb-64' }),
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
renderEditor: (content) => {
|
renderEditor: (content) => {
|
||||||
@@ -560,7 +577,7 @@ export const iconByType: Record<FileType, string> = {
|
|||||||
|
|
||||||
export class Editor
|
export class Editor
|
||||||
{
|
{
|
||||||
tree: TreeDOM;
|
tree!: TreeDOM;
|
||||||
container: HTMLDivElement;
|
container: HTMLDivElement;
|
||||||
|
|
||||||
selected?: Recursive<LocalContent & { element?: HTMLElement }>;
|
selected?: Recursive<LocalContent & { element?: HTMLElement }>;
|
||||||
@@ -680,29 +697,31 @@ export class Editor
|
|||||||
},
|
},
|
||||||
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
|
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
|
||||||
|
|
||||||
this.tree = new TreeDOM((item, depth) => {
|
Content.ready.then(() => {
|
||||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
|
this.tree = new TreeDOM((item, depth) => {
|
||||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
|
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
|
||||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
|
||||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
|
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
|
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
|
||||||
])]);
|
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
|
||||||
}, (item, depth) => {
|
])]);
|
||||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
|
}, (item, depth) => {
|
||||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
|
||||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
|
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
|
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
|
||||||
])]);
|
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
|
||||||
|
])]);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
|
||||||
|
|
||||||
|
this.cleanup = this.setupDnD();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instruction = dom('div', { class: 'absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50' });
|
this.instruction = dom('div', { class: 'absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50' });
|
||||||
|
|
||||||
this.cleanup = this.setupDnD();
|
|
||||||
|
|
||||||
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
|
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
|
||||||
|
|
||||||
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
|
|
||||||
}
|
}
|
||||||
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
|
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
|
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
||||||
import { Annotation, EditorState, SelectionRange, type Range } from '@codemirror/state';
|
import { Annotation, EditorState, SelectionRange, StateField, type Range } from '@codemirror/state';
|
||||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
import { bracketMatching, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
||||||
import { search, searchKeymap } from '@codemirror/search';
|
import { search, searchKeymap } from '@codemirror/search';
|
||||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||||||
import { lintKeymap } from '@codemirror/lint';
|
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { IterMode, Tree } from '@lezer/common';
|
import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
import { dom } from './dom.util';
|
import { dom } from '#shared/dom.util';
|
||||||
|
import { callout as calloutExtension } from '#shared/grammar/callout.extension';
|
||||||
|
import { wikilink as wikilinkExtension } from '#shared/grammar/wikilink.extension';
|
||||||
|
import { renderMarkdown } from '#shared/markdown.util';
|
||||||
|
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses";
|
||||||
|
import { tagTag, tag as tagExtension } from './grammar/tag.extension';
|
||||||
|
|
||||||
const External = Annotation.define<boolean>();
|
const External = Annotation.define<boolean>();
|
||||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||||
@@ -28,15 +32,88 @@ const highlight = HighlightStyle.define([
|
|||||||
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
|
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
|
||||||
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
|
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
|
||||||
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
|
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
|
||||||
{ tag: tags.meta, color: "#404740" },
|
{ tag: tags.meta, class: 'text-light-60 dark:text-dark-60' },
|
||||||
{ tag: tags.link, textDecoration: "underline" },
|
{ tag: tags.link, class: 'text-accent-blue hover:underline' },
|
||||||
|
{ tag: tags.special(tags.link), class: 'text-accent-blue font-semibold' },
|
||||||
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
|
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
|
||||||
{ tag: tags.emphasis, fontStyle: "italic" },
|
{ tag: tags.emphasis, fontStyle: "italic" },
|
||||||
{ tag: tags.strong, fontWeight: "bold" },
|
{ tag: tags.strong, fontWeight: "bold" },
|
||||||
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||||
{ tag: tags.keyword, color: "#708" },
|
{ tag: tags.keyword, class: "text-accent-blue" },
|
||||||
|
{ tag: tags.monospace, class: "border border-light-35 dark:border-dark-35 px-2 py-px rounded-sm bg-light-20 dark:bg-dark-20" },
|
||||||
|
{ tag: tagTag, class: "cursor-default bg-accent-blue bg-opacity-10 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
class CalloutWidget extends WidgetType
|
||||||
|
{
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
foldable?: boolean;
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
contentMD: HTMLElement;
|
||||||
|
|
||||||
|
static create(node: SyntaxNodeRef, state: EditorState): CalloutWidget | undefined
|
||||||
|
{
|
||||||
|
let type = '';
|
||||||
|
let title = '';
|
||||||
|
const content: string[] = [];
|
||||||
|
|
||||||
|
let cursor = node.node.cursor();
|
||||||
|
if (!cursor.firstChild()) return undefined;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (cursor.name === 'CalloutMarker')
|
||||||
|
{
|
||||||
|
const _cursor = cursor.node.cursor();
|
||||||
|
_cursor.lastChild();
|
||||||
|
type = state.doc.sliceString(_cursor.from, _cursor.to).toLowerCase();
|
||||||
|
}
|
||||||
|
else if (cursor.name === 'CalloutTitle')
|
||||||
|
{
|
||||||
|
title = state.doc.sliceString(cursor.from, cursor.to).trim();
|
||||||
|
}
|
||||||
|
else if (cursor.name === 'CalloutLine')
|
||||||
|
{
|
||||||
|
const _cursor = cursor.node.cursor();
|
||||||
|
_cursor.lastChild();
|
||||||
|
content.push(state.doc.sliceString(_cursor.from, _cursor.to));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (cursor.nextSibling());
|
||||||
|
|
||||||
|
return new CalloutWidget(node.from, node.to, title || (type.substring(0, 1).toUpperCase() + type.substring(1).toLowerCase()), type, content.join('\n'));
|
||||||
|
}
|
||||||
|
constructor(from: number, to: number, title: string, type: string, content: string, foldable?: boolean)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
|
||||||
|
this.title = title;
|
||||||
|
this.type = type;
|
||||||
|
this.content = content;
|
||||||
|
this.foldable = foldable;
|
||||||
|
|
||||||
|
this.contentMD = renderMarkdown(useMarkdown().parseSync(content), { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th });
|
||||||
|
}
|
||||||
|
override eq(other: CalloutWidget)
|
||||||
|
{
|
||||||
|
return this.from === other.from && this.to === other.to;
|
||||||
|
}
|
||||||
|
toDOM(view: EditorView)
|
||||||
|
{
|
||||||
|
return dom('div', { class: 'flex cm-line', listeners: { click: e => view.dispatch({ selection: { anchor: this.from, head: this.to } }) } }, [prose('blockquote', callout, [ this.contentMD ], { title: this.title, type: this.type, fold: this.foldable, class: '!m-px ' }) as HTMLElement | undefined]);
|
||||||
|
}
|
||||||
|
override ignoreEvent(event: Event)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
class Decorator
|
class Decorator
|
||||||
{
|
{
|
||||||
static hiddenNodes: string[] = [
|
static hiddenNodes: string[] = [
|
||||||
@@ -46,11 +123,15 @@ class Decorator
|
|||||||
'CodeMark',
|
'CodeMark',
|
||||||
'CodeInfo',
|
'CodeInfo',
|
||||||
'URL',
|
'URL',
|
||||||
|
'CalloutMark',
|
||||||
|
'WikilinkMeta',
|
||||||
|
'WikilinkHref',
|
||||||
|
'TagMeta'
|
||||||
]
|
]
|
||||||
decorations: DecorationSet;
|
decorations: DecorationSet;
|
||||||
constructor(view: EditorView)
|
constructor(view: EditorView)
|
||||||
{
|
{
|
||||||
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
|
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, [], view.state), true);
|
||||||
}
|
}
|
||||||
update(update: ViewUpdate)
|
update(update: ViewUpdate)
|
||||||
{
|
{
|
||||||
@@ -59,14 +140,14 @@ class Decorator
|
|||||||
|
|
||||||
this.decorations = this.decorations.update({
|
this.decorations = this.decorations.update({
|
||||||
filter: (f, t, v) => false,
|
filter: (f, t, v) => false,
|
||||||
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
|
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges, update.state),
|
||||||
sort: true,
|
sort: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
iterate(tree: Tree, visible: readonly {
|
iterate(tree: Tree, visible: readonly {
|
||||||
from: number;
|
from: number;
|
||||||
to: number;
|
to: number;
|
||||||
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
|
}[], selection: readonly SelectionRange[], state: EditorState): Range<Decoration>[]
|
||||||
{
|
{
|
||||||
const decorations: Range<Decoration>[] = [];
|
const decorations: Range<Decoration>[] = [];
|
||||||
|
|
||||||
@@ -74,7 +155,7 @@ class Decorator
|
|||||||
tree.iterate({
|
tree.iterate({
|
||||||
from, to, mode: IterMode.IgnoreMounts,
|
from, to, mode: IterMode.IgnoreMounts,
|
||||||
enter: node => {
|
enter: node => {
|
||||||
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
|
if(node.node.parent && node.node.parent.name !== 'Document' && selection.some(e => intersects(e, node.node.parent!)))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
else if(node.name === 'HeaderMark')
|
else if(node.name === 'HeaderMark')
|
||||||
@@ -97,20 +178,56 @@ class Decorator
|
|||||||
return decorations;
|
return decorations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function blockIterate(tree: Tree, state: EditorState): Range<Decoration>[]
|
||||||
|
{
|
||||||
|
const decorations: Range<Decoration>[] = [];
|
||||||
|
const selection = state.selection.ranges;
|
||||||
|
|
||||||
|
tree.iterate({
|
||||||
|
mode: IterMode.IgnoreMounts,
|
||||||
|
enter: node => {
|
||||||
|
if(selection.some(e => intersects(e, node)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
else if(node.name === 'CalloutBlock')
|
||||||
|
return decorations.push(Decoration.replace({ widget: CalloutWidget.create(node, state), block: true, }).range(node.from, node.to)), false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return decorations;
|
||||||
|
}
|
||||||
|
const BlockDecorator = StateField.define<DecorationSet>({
|
||||||
|
create(state)
|
||||||
|
{
|
||||||
|
return Decoration.set(blockIterate(syntaxTree(state), state), true);
|
||||||
|
},
|
||||||
|
update(decorations, transaction)
|
||||||
|
{
|
||||||
|
if(transaction.docChanged || transaction.selection)
|
||||||
|
return Decoration.set(blockIterate(syntaxTree(transaction.state), transaction.state), true);
|
||||||
|
return decorations.map(transaction.changes);
|
||||||
|
},
|
||||||
|
provide: f => EditorView.decorations.from(f),
|
||||||
|
})
|
||||||
|
|
||||||
export class MarkdownEditor
|
export class MarkdownEditor
|
||||||
{
|
{
|
||||||
private static _singleton: MarkdownEditor;
|
private static _singleton: MarkdownEditor;
|
||||||
|
|
||||||
private view: EditorView;
|
private view: EditorView;
|
||||||
|
private viewer: 'read' | 'live' | 'edit' = 'live';
|
||||||
onChange?: (content: string) => void;
|
onChange?: (content: string) => void;
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this.view = new EditorView({
|
this.view = new EditorView({
|
||||||
extensions: [
|
extensions: [
|
||||||
markdown({
|
markdown({
|
||||||
base: markdownLanguage
|
base: markdownLanguage,
|
||||||
|
extensions: [ calloutExtension, wikilinkExtension, tagExtension ]
|
||||||
}),
|
}),
|
||||||
|
BlockDecorator,
|
||||||
history(),
|
history(),
|
||||||
search(),
|
search(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
@@ -126,9 +243,7 @@ export class MarkdownEditor
|
|||||||
...defaultKeymap,
|
...defaultKeymap,
|
||||||
...searchKeymap,
|
...searchKeymap,
|
||||||
...historyKeymap,
|
...historyKeymap,
|
||||||
...foldKeymap,
|
...completionKeymap
|
||||||
...completionKeymap,
|
|
||||||
...lintKeymap
|
|
||||||
]),
|
]),
|
||||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MarkdownEditor } from "#shared/editor.util";
|
|||||||
import { preview } from "#shared/proses";
|
import { preview } from "#shared/proses";
|
||||||
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
|
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
|
||||||
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||||
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, weaponTypeTexts } from "#shared/character.util";
|
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, categoryText, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, weaponTypeTexts } from "#shared/character.util";
|
||||||
import characterConfig from "#shared/character-config.json";
|
import characterConfig from "#shared/character-config.json";
|
||||||
import { getID } from "#shared/general.util";
|
import { getID } from "#shared/general.util";
|
||||||
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
|
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
|
||||||
@@ -13,18 +13,6 @@ import { getText } from "#shared/i18n";
|
|||||||
|
|
||||||
type Category = ItemConfig['category'];
|
type Category = ItemConfig['category'];
|
||||||
type Rarity = ItemConfig['rarity'];
|
type Rarity = ItemConfig['rarity'];
|
||||||
const categoryText: Record<Category, string> = {
|
|
||||||
'mundane': 'Objet inerte',
|
|
||||||
'armor': 'Armure',
|
|
||||||
'weapon': 'Arme',
|
|
||||||
'wondrous': 'Objet magique'
|
|
||||||
};
|
|
||||||
const rarityText: Record<Rarity, string> = {
|
|
||||||
'common': 'Commun',
|
|
||||||
'uncommon': 'Peu commun',
|
|
||||||
'rare': 'Rare',
|
|
||||||
'legendary': 'Légendaire'
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
export class HomebrewBuilder
|
export class HomebrewBuilder
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface FloatingProperties
|
|||||||
style?: Record<string, string | undefined | boolean | number> | string;
|
style?: Record<string, string | undefined | boolean | number> | string;
|
||||||
viewport?: HTMLElement;
|
viewport?: HTMLElement;
|
||||||
cover?: 'width' | 'height' | 'all' | 'none';
|
cover?: 'width' | 'height' | 'all' | 'none';
|
||||||
|
persistant?: boolean;
|
||||||
}
|
}
|
||||||
export interface FollowerProperties extends FloatingProperties
|
export interface FollowerProperties extends FloatingProperties
|
||||||
{
|
{
|
||||||
@@ -36,17 +37,35 @@ export interface ModalProperties
|
|||||||
closeWhenOutside?: boolean;
|
closeWhenOutside?: boolean;
|
||||||
onClose?: () => boolean | void;
|
onClose?: () => boolean | void;
|
||||||
}
|
}
|
||||||
|
type ModalInternals = {
|
||||||
|
container: HTMLElement;
|
||||||
|
content: HTMLElement;
|
||||||
|
stop: Function;
|
||||||
|
start: Function;
|
||||||
|
show: Function;
|
||||||
|
hide: Function;
|
||||||
|
persistant: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
let teleport: HTMLDivElement;
|
export let teleport: HTMLDivElement, minimizeBox: HTMLDivElement, cache: ModalInternals[] = [];
|
||||||
export function init()
|
export function init()
|
||||||
{
|
{
|
||||||
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
|
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
|
||||||
|
minimizeBox = dom('div', { attributes: { id: 'minimize-container' }, class: 'absolute bottom-0 left-0 flex flex-row px-4 gap-4 z-40 h-[21px]' });
|
||||||
|
cache = [];
|
||||||
document.body.appendChild(teleport);
|
document.body.appendChild(teleport);
|
||||||
|
document.body.appendChild(minimizeBox);
|
||||||
|
|
||||||
|
useRouter().afterEach(clear);
|
||||||
|
}
|
||||||
|
function clear()
|
||||||
|
{
|
||||||
|
cache = cache.filter(e => !(!e.persistant && e.content.remove()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function popper(container: HTMLElement, properties?: PopperProperties)
|
export function popper(container: HTMLElement, properties?: PopperProperties)
|
||||||
{
|
{
|
||||||
let state: FloatState = 'hidden', manualStop = false, timeout: Timer;
|
let state: FloatState = 'hidden', timeout: Timer;
|
||||||
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
||||||
const content = dom('div', { class: properties?.class, style: properties?.style });
|
const content = dom('div', { class: properties?.class, style: properties?.style });
|
||||||
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
|
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
|
||||||
@@ -201,7 +220,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
|
|||||||
link(container);
|
link(container);
|
||||||
link(floater);
|
link(floater);
|
||||||
|
|
||||||
return { container, content: floater, stop, start, show: () => {
|
const result = { container, content: floater, stop, start, show: () => {
|
||||||
if(typeof properties?.content === 'function')
|
if(typeof properties?.content === 'function')
|
||||||
properties.content = properties.content();
|
properties.content = properties.content();
|
||||||
|
|
||||||
@@ -228,11 +247,13 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
|
|||||||
floater.setAttribute('data-state', 'closed');
|
floater.setAttribute('data-state', 'closed');
|
||||||
floater.classList.toggle('hidden', true);
|
floater.classList.toggle('hidden', true);
|
||||||
|
|
||||||
manualStop = false;
|
|
||||||
floater.toggleAttribute('data-pinned', false);
|
floater.toggleAttribute('data-pinned', false);
|
||||||
|
|
||||||
state = 'hidden';
|
state = 'hidden';
|
||||||
} };
|
} };
|
||||||
|
|
||||||
|
cache.push({ ...result, persistant: properties?.persistant ?? false });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function format(date: Date, template: string): string
|
|||||||
|
|
||||||
for(const key of keys)
|
for(const key of keys)
|
||||||
{
|
{
|
||||||
template = template.replaceAll(key, () => transforms[key](date));
|
template = template.replaceAll(key, () => transforms[key]!(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
|
|||||||
57
shared/grammar/callout.extension.ts
Normal file
57
shared/grammar/callout.extension.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { MarkdownConfig } from '@lezer/markdown';
|
||||||
|
import { styleTags, tags } from '@lezer/highlight';
|
||||||
|
|
||||||
|
export const callout: MarkdownConfig = {
|
||||||
|
defineNodes: [
|
||||||
|
'CalloutBlock',
|
||||||
|
'CalloutMarker',
|
||||||
|
'CalloutMark',
|
||||||
|
'CalloutType',
|
||||||
|
'CalloutTitle',
|
||||||
|
'CalloutLine',
|
||||||
|
'CalloutContent',
|
||||||
|
],
|
||||||
|
parseBlock: [{
|
||||||
|
name: 'Callout',
|
||||||
|
before: 'Blockquote',
|
||||||
|
parse(cx, line) {
|
||||||
|
const match = /^>\s*\[!(\w+)\](?:\s+(.*))?/.exec(line.text);
|
||||||
|
if (!match || !match[1]) return false; //No match
|
||||||
|
|
||||||
|
const start = cx.lineStart, children = [];
|
||||||
|
|
||||||
|
const quoteEnd = start + line.text.indexOf('[!');
|
||||||
|
const typeStart = quoteEnd + 2;
|
||||||
|
const typeEnd = typeStart + match[1].length;
|
||||||
|
const bracketEnd = typeEnd + 1;
|
||||||
|
|
||||||
|
children.push(cx.elt('CalloutMarker', start, bracketEnd, [ cx.elt('CalloutMark', start, quoteEnd), cx.elt('CalloutType', typeStart, typeEnd) ]));
|
||||||
|
|
||||||
|
if(match[2]) children.push(cx.elt('CalloutTitle', bracketEnd + 1, start + line.text.length));
|
||||||
|
|
||||||
|
while (cx.nextLine() && line.text.startsWith('>'))
|
||||||
|
{
|
||||||
|
const pos = line.text.substring(1).search(/\S/) + 1;
|
||||||
|
children.push(cx.elt('CalloutLine', cx.lineStart, cx.lineStart + line.text.length, [
|
||||||
|
cx.elt('CalloutMark', cx.lineStart, cx.lineStart + pos),
|
||||||
|
cx.elt('CalloutContent', cx.lineStart + pos, cx.lineStart + line.text.length),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.addElement(cx.elt('CalloutBlock', start, cx.lineStart - 1, children));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
'CalloutBlock': tags.special(tags.quote),
|
||||||
|
'CalloutMarker': tags.meta,
|
||||||
|
'CalloutMark': tags.meta,
|
||||||
|
'CalloutType': tags.keyword,
|
||||||
|
'CalloutTitle': tags.heading,
|
||||||
|
'CalloutLine': tags.content,
|
||||||
|
'CalloutContent': tags.content,
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
27
shared/grammar/tag.extension.ts
Normal file
27
shared/grammar/tag.extension.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { MarkdownConfig } from '@lezer/markdown';
|
||||||
|
import { styleTags, Tag, tags } from '@lezer/highlight';
|
||||||
|
|
||||||
|
export const tagTag = Tag.define('tag');
|
||||||
|
export const tag: MarkdownConfig = {
|
||||||
|
defineNodes: [
|
||||||
|
'Tag',
|
||||||
|
'TagMeta',
|
||||||
|
],
|
||||||
|
parseInline: [{
|
||||||
|
name: 'Tag',
|
||||||
|
parse(cx, next, pos)
|
||||||
|
{
|
||||||
|
//35 == '#'
|
||||||
|
if (cx.slice(pos, pos + 1).charCodeAt(0) !== 35 || String.fromCharCode(next).trim() === '') return -1;
|
||||||
|
|
||||||
|
const end = cx.slice(pos, cx.end).search(/\s/);
|
||||||
|
return cx.addElement(cx.elt('Tag', pos, end === -1 ? cx.end : end, [ cx.elt('TagMeta', pos, pos + 1) ]));
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
'Tag': tagTag,
|
||||||
|
'TagMeta': tags.meta,
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
71
shared/grammar/wikilink.extension.ts
Normal file
71
shared/grammar/wikilink.extension.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Element, MarkdownConfig } from '@lezer/markdown';
|
||||||
|
import { styleTags, tags } from '@lezer/highlight';
|
||||||
|
|
||||||
|
export const wikilink: MarkdownConfig = {
|
||||||
|
defineNodes: [
|
||||||
|
'Wikilink',
|
||||||
|
'WikilinkMeta',
|
||||||
|
'WikilinkHref',
|
||||||
|
'WikilinkTitle',
|
||||||
|
],
|
||||||
|
parseInline: [{
|
||||||
|
name: 'Wikilink',
|
||||||
|
before: 'Link',
|
||||||
|
parse(cx, next, pos)
|
||||||
|
{
|
||||||
|
// 91 == '['
|
||||||
|
if (next !== 91 || cx.slice(pos, pos + 1).charCodeAt(0) !== 91) return -1;
|
||||||
|
|
||||||
|
const match = /!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/.exec(cx.slice(pos, cx.end));
|
||||||
|
if(!match) return -1;
|
||||||
|
|
||||||
|
const start = pos, children: Element[] = [], end = start + match[0].length;
|
||||||
|
|
||||||
|
children.push(cx.elt('WikilinkMeta', start, start + 2));
|
||||||
|
|
||||||
|
if(match[1] && !match[2] && !match[3]) //Link only
|
||||||
|
{
|
||||||
|
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
|
||||||
|
}
|
||||||
|
else if(!match[1] && match[2] && match[3]) //Hash and title
|
||||||
|
{
|
||||||
|
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[2].length));
|
||||||
|
children.push(cx.elt('WikilinkMeta', start + 2 + match[2].length, start + 2 + match[2].length + 1));
|
||||||
|
children.push(cx.elt('WikilinkTitle', start + 2 + match[2].length + 1, start + 2 + match[2].length + match[3].length));
|
||||||
|
}
|
||||||
|
else if(!match[1] && !match[2] && match[3]) //Hash only
|
||||||
|
{
|
||||||
|
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
|
||||||
|
}
|
||||||
|
else if(match[1] && match[2] && !match[3]) //Link and hash
|
||||||
|
{
|
||||||
|
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
|
||||||
|
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length, start + 2 + match[1].length + match[2].length));
|
||||||
|
}
|
||||||
|
else if(match[1] && !match[2] && match[3]) //Link and title
|
||||||
|
{
|
||||||
|
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
|
||||||
|
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length, start + 2 + match[1].length + 1));
|
||||||
|
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + 1, start + 2 + match[1].length + match[3].length));
|
||||||
|
}
|
||||||
|
else if(match[1] && match[2] && match[3]) //Link, hash and title
|
||||||
|
{
|
||||||
|
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length + match[2].length));
|
||||||
|
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length + match[2].length, start + 2 + match[1].length + match[2].length + 1));
|
||||||
|
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + match[2].length + 1, start + 2 + match[1].length + match[2].length + match[3].length));
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(cx.elt('WikilinkMeta', end - 2, end));
|
||||||
|
|
||||||
|
return cx.addElement(cx.elt('Wikilink', start, end, children));
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
props: [
|
||||||
|
styleTags({
|
||||||
|
'Wikilink': tags.special(tags.content),
|
||||||
|
'WikilinkMeta': tags.meta,
|
||||||
|
'WikilinkHref': tags.link,
|
||||||
|
'WikilinkTitle': tags.special(tags.link),
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -64,7 +64,7 @@ export function markdownReference(content: string, filter?: string, properties?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
|
const el = renderMarkdown(data, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags));
|
||||||
|
|
||||||
if(properties) styling(el, properties);
|
if(properties) styling(el, properties);
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const callout: Prose = {
|
|||||||
} = properties;
|
} = properties;
|
||||||
|
|
||||||
let open = fold;
|
let open = fold;
|
||||||
const container = dom('div', { class: 'callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
|
const container = dom('div', { class: ['callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', properties?.class], attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
|
||||||
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => {
|
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => {
|
||||||
container.setAttribute('data-state', open ? 'open' : 'closed');
|
container.setAttribute('data-state', open ? 'open' : 'closed');
|
||||||
open = !open;
|
open = !open;
|
||||||
|
|||||||
4
types/character.d.ts
vendored
4
types/character.d.ts
vendored
@@ -52,11 +52,13 @@ export type CharacterVariables = {
|
|||||||
poisons: Array<{ id: string, state: number | true }>;
|
poisons: Array<{ id: string, state: number | true }>;
|
||||||
spells: string[]; //Spell ID
|
spells: string[]; //Spell ID
|
||||||
items: ItemState[];
|
items: ItemState[];
|
||||||
|
|
||||||
|
money: number;
|
||||||
};
|
};
|
||||||
type ItemState = {
|
type ItemState = {
|
||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
enchantments?: [];
|
enchantments?: string[];
|
||||||
charges?: number;
|
charges?: number;
|
||||||
equipped?: boolean;
|
equipped?: boolean;
|
||||||
state?: any;
|
state?: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user