You've already forked obsidian-visualiser
Compare commits
5 Commits
rework
...
0c17dbf7bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c17dbf7bc | |||
| ac17134b7e | |||
| adb37b255a | |||
| b54402fc19 | |||
| 0882eb1dd0 |
66
components/base/DropdownContentRender.vue
Normal file
66
components/base/DropdownContentRender.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<template v-for="(item, idx) of options">
|
||||||
|
<template v-if="item.type === 'item'">
|
||||||
|
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<span v-if="item.kbd"> {{ item.kbd }} </span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="item.type === 'checkbox'">
|
||||||
|
<DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" @update:checked="item.select">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon icon="radix-icons:check" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<span v-if="item.kbd"> {{ item.kbd }} </span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="item.type === 'radio'">
|
||||||
|
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuRadioGroup @update:model-value="item.change">
|
||||||
|
<DropdownMenuRadioItem v-for="option in item.items" :disabled="(option as any)?.disabled ?? false" :value="(option as any)?.value ?? option">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon icon="radix-icons:dot-filled" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
<span>{{ (option as any)?.label || option }}</span>
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator v-if="idx !== options.length - 1" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="item.type === 'submenu'">
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<Icon icon="radix-icons:chevron-right" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownContentRender :options="item.items" />
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="item.type === 'group'">
|
||||||
|
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
|
||||||
|
<DropdownContetnRender :options="item.items" />
|
||||||
|
|
||||||
|
<DropdownMenuSeparator v-if="idx !== options.length - 1" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownOption } from './DropdownMenu.vue';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const { options } = defineProps<{
|
||||||
|
options: DropdownOption[]
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
59
components/base/DropdownMenu.vue
Normal file
59
components/base/DropdownMenu.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<DropdownMenuRoot>
|
||||||
|
<DropdownMenuTrigger :disabled="disabled" ><slot /></DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent :align="align" :side="side">
|
||||||
|
<DropdownContentRender :options="options" />
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuArrow />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
export interface DropdownItem {
|
||||||
|
type: 'item';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
select?: () => void;
|
||||||
|
icon?: string;
|
||||||
|
kbd?: string;
|
||||||
|
}
|
||||||
|
export interface DropdownCheckbox {
|
||||||
|
type: 'checkbox';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
checked?: boolean | Ref<boolean>
|
||||||
|
select?: (state: boolean) => void;
|
||||||
|
kbd?: string;
|
||||||
|
}
|
||||||
|
export interface DropdownRadioGroup {
|
||||||
|
type: 'radio';
|
||||||
|
label: string;
|
||||||
|
value?: string | Ref<string>
|
||||||
|
items: (string | {label: string, value?: string, disabled?: boolean})[];
|
||||||
|
change?: (value: string) => void;
|
||||||
|
}
|
||||||
|
export interface DropdownSubmenu {
|
||||||
|
type: 'submenu';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
items: DropdownOption[];
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
export interface DropdownGroup {
|
||||||
|
type: 'group';
|
||||||
|
label?: string;
|
||||||
|
items: DropdownOption[];
|
||||||
|
}
|
||||||
|
export type DropdownOption = DropdownItem | DropdownCheckbox | DropdownRadioGroup | DropdownSubmenu | DropdownGroup;
|
||||||
|
const { options, disabled = false, side, align } = defineProps<{
|
||||||
|
options: DropdownOption[]
|
||||||
|
disabled?: boolean
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
align?: 'start' | 'center' | 'end'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label">
|
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label" :defaultExpanded="flatten(model)">
|
||||||
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
|
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
|
||||||
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren, 'font-medium': item.hasChildren }" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren, 'font-medium': item.hasChildren }" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||||
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
|
||||||
@@ -19,9 +19,15 @@ interface TreeItem
|
|||||||
label: string
|
label: string
|
||||||
link?: string
|
link?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
|
open?: boolean
|
||||||
children?: TreeItem[]
|
children?: TreeItem[]
|
||||||
}
|
}
|
||||||
const model = defineModel<TreeItem[]>();
|
const model = defineModel<TreeItem[]>();
|
||||||
|
|
||||||
|
function flatten(arr: TreeItem[]): string[]
|
||||||
|
{
|
||||||
|
return arr.filter(e => e.open).flatMap(e => [e?.link ?? e.label, ...flatten(e.children ?? [])]);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
||||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
import { clamp } from '#imports';
|
import { clamp } from '#shared/general.utils';
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@@ -150,24 +150,20 @@ dark:border-dark-purple
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const dragHandler = useDrag(({ event: Event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
const pinchHandler = usePinch(({ event, offset: [z] }: { event: Event, offset: number[] }) => {
|
||||||
event?.preventDefault();
|
zoom.value = clamp(z / 2048, minZoom.value, 3);
|
||||||
|
}, {
|
||||||
|
domTarget: canvas,
|
||||||
|
eventOptions: { passive: false, }
|
||||||
|
})
|
||||||
|
const dragHandler = useDrag(({ event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
||||||
dispX.value += x / zoom.value;
|
dispX.value += x / zoom.value;
|
||||||
dispY.value += y / zoom.value;
|
dispY.value += y / zoom.value;
|
||||||
}, {
|
}, {
|
||||||
domTarget: canvas,
|
domTarget: canvas,
|
||||||
eventOptions: { passive: false, }
|
eventOptions: { passive: false, }
|
||||||
})
|
})
|
||||||
const pinchHandler = usePinch(({ event: Event, offset: [z] }: { event: Event, offset: number[] }) => {
|
const wheelHandler = useWheel(({ event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
||||||
event?.preventDefault();
|
|
||||||
console.log(z);
|
|
||||||
zoom.value = clamp(z / 2048, minZoom.value, 3);
|
|
||||||
}, {
|
|
||||||
domTarget: canvas,
|
|
||||||
eventOptions: { passive: false, }
|
|
||||||
})
|
|
||||||
const wheelHandler = useWheel(({ event: Event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
|
||||||
event?.preventDefault();
|
|
||||||
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
|
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
|
||||||
}, {
|
}, {
|
||||||
domTarget: canvas,
|
domTarget: canvas,
|
||||||
@@ -180,7 +176,7 @@ const wheelHandler = useWheel(({ event: Event, delta: [x, y] }: { event: Event,
|
|||||||
<template #default>
|
<template #default>
|
||||||
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
||||||
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-30 overflow-hidden">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden">
|
||||||
<Tooltip message="Zoom avant" side="right">
|
<Tooltip message="Zoom avant" side="right">
|
||||||
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
<Icon icon="radix-icons:plus" />
|
<Icon icon="radix-icons:plus" />
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseId } from '#shared/general.utils';
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseId } from '#shared/general.utils';
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
const generate = computed(() => props.id)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseId } from '#shared/general.utils';
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
const generate = computed(() => props.id)
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseId } from '#shared/general.utils';
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseId } from '#shared/general.utils';
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
const generate = computed(() => props.id)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { parseId } from '#shared/general.utils';
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
const generate = computed(() => props.id)
|
||||||
|
|||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
@@ -39,8 +39,9 @@ export const explorerContentTable = sqliteTable("explorer_content", {
|
|||||||
title: text().notNull(),
|
title: text().notNull(),
|
||||||
type: text({ enum: ['file', 'folder', 'markdown', 'canvas'] }).notNull(),
|
type: text({ enum: ['file', 'folder', 'markdown', 'canvas'] }).notNull(),
|
||||||
content: blob({ mode: 'buffer' }),
|
content: blob({ mode: 'buffer' }),
|
||||||
navigable: int({ mode: 'boolean' }).default(true),
|
navigable: int({ mode: 'boolean' }).notNull().default(true),
|
||||||
private: int({ mode: 'boolean' }).default(false),
|
private: int({ mode: 'boolean' }).notNull().default(false),
|
||||||
|
order: int().unique('order').notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
|
|||||||
2
drizzle/0003_cultured_skaar.sql
Normal file
2
drizzle/0003_cultured_skaar.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `explorer_content` ADD `order` integer;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `order` ON `explorer_content` (`order`);
|
||||||
313
drizzle/meta/0003_snapshot.json
Normal file
313
drizzle/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "a1a7b478-d0c3-4fc6-b74a-1a010c1d8ca1",
|
||||||
|
"prevId": "6da7ff20-0db8-4055-a353-bb0ea2fa5e0b",
|
||||||
|
"tables": {
|
||||||
|
"explorer_content": {
|
||||||
|
"name": "explorer_content",
|
||||||
|
"columns": {
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"navigable": {
|
||||||
|
"name": "navigable",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"columns": [
|
||||||
|
"order"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"explorer_content_owner_users_id_fk": {
|
||||||
|
"name": "explorer_content_owner_users_id_fk",
|
||||||
|
"tableFrom": "explorer_content",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1730985155814,
|
"when": 1730985155814,
|
||||||
"tag": "0002_messy_solo",
|
"tag": "0002_messy_solo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1731344368953,
|
||||||
|
"tag": "0003_cultured_skaar",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="text-3xl">Une erreur est survenue.</div>
|
<div class="text-3xl">Une erreur est survenue.</div>
|
||||||
</div>
|
</div>
|
||||||
<pre class="">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
<pre class="">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
||||||
<NuxtLink :href="{ name: 'index' }"><Button>Revenir en lieu sûr</Button></NuxtLink>
|
<Button @click="handleError">Revenir en lieu sûr</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||||
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
<Tooltip v-if="!loggedIn" :message="'Se connecter'" side="right">
|
||||||
<NuxtLink class="" :to="{ name: 'user-profile' }">
|
|
||||||
<div class="hover:border-opacity-70 flex">
|
<div class="hover:border-opacity-70 flex">
|
||||||
<Icon :icon="loggedIn ? 'radix-icons:avatar' : 'radix-icons:person'" class="w-7 h-7 p-1" />
|
<Icon :icon="'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</Tooltip>
|
||||||
|
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||||
|
<DropdownMenu :options="[{
|
||||||
|
type: 'group',
|
||||||
|
items: [{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Mon profil',
|
||||||
|
click: () => useRouter().push({ name: 'user-profile' }),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Deconnexion',
|
||||||
|
click: () => clear(),
|
||||||
|
}]
|
||||||
|
}]">
|
||||||
|
<div class="hover:border-opacity-70 flex">
|
||||||
|
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenu>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,17 +48,40 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex gap-4 items-center">
|
||||||
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>
|
||||||
<Tooltip :message="loggedIn ? 'Mon profil' : 'Se connecter'" side="right">
|
<Tooltip v-if="!loggedIn" :message="'Se connecter'" side="right">
|
||||||
<NuxtLink class="" :to="{ name: 'user-profile' }">
|
<NuxtLink :to="{ name: 'user-login' }">
|
||||||
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||||
<Icon :icon="loggedIn ? 'radix-icons:avatar' : 'radix-icons:person'" class="w-7 h-7 p-1" />
|
<Icon :icon="'radix-icons:person'" class="w-7 h-7 p-1" />
|
||||||
</div>
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip v-else :message="'Mon profil'" side="right">
|
||||||
|
<DropdownMenu :options="[{
|
||||||
|
type: 'group',
|
||||||
|
items: [{
|
||||||
|
type: 'item',
|
||||||
|
label: 'Mon profil',
|
||||||
|
click: () => useRouter().push({ name: 'user-profile' }),
|
||||||
|
}, {
|
||||||
|
type: 'item',
|
||||||
|
label: 'Deconnexion',
|
||||||
|
click: () => clear(),
|
||||||
|
}]
|
||||||
|
}]">
|
||||||
|
<div class="bg-light-20 dark:bg-dark-20 hover:border-opacity-70 flex border p-px border-light-50 dark:border-dark-50">
|
||||||
|
<Icon :icon="'radix-icons:avatar'" class="w-7 h-7 p-1" />
|
||||||
|
</div>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tree v-if="pages" v-model="pages" class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden"/>
|
<div class="flex-1 xl:px-6 px-3 max-w-full max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
|
<NuxtLink :href="{ name: 'explore' }" no-prefetch class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
|
||||||
|
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
|
||||||
|
</NuxtLink>
|
||||||
|
<Tree v-if="pages" v-model="pages"/>
|
||||||
|
</div>
|
||||||
<div class="xl:px-12 px-6 text-start text-xs text-light-60 dark:text-dark-60 relative top-4">
|
<div class="xl:px-12 px-6 text-start text-xs text-light-60 dark:text-dark-60 relative top-4">
|
||||||
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||||
<p>Copyright Peaceultime - 2024</p>
|
<p>Copyright Peaceultime - 2024</p>
|
||||||
@@ -57,19 +96,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
const open = ref(true);
|
const open = ref(false);
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn, clear } = useUserSession();
|
||||||
|
|
||||||
|
const route = useRouter().currentRoute;
|
||||||
|
const path = computed(() => route.value.params.path ? Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path : undefined);
|
||||||
|
|
||||||
|
watch(route, () => {
|
||||||
|
open.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
const { data: pages } = await useLazyFetch('/api/navigation', {
|
const { data: pages } = await useLazyFetch('/api/navigation', {
|
||||||
transform: transform,
|
transform: transform,
|
||||||
});
|
watch: [useRouter().currentRoute]
|
||||||
|
|
||||||
watch(useRouter().currentRoute, () => {
|
|
||||||
open.value = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function transform(list: any[]): any[]
|
function transform(list: any[]): any[]
|
||||||
{
|
{
|
||||||
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'private' : e.type }))
|
return list?.map(e => ({ label: e.title, children: transform(e.children), link: e.path, tag: e.private ? 'private' : e.type, open: path.value?.startsWith(e.path)}))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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 meta = to.meta;
|
const meta = to.meta;
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -10,20 +10,36 @@
|
|||||||
"@iconify/vue": "^4.1.2",
|
"@iconify/vue": "^4.1.2",
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||||
|
"@vueuse/gesture": "^2.0.0",
|
||||||
"@vueuse/nuxt": "^11.1.0",
|
"@vueuse/nuxt": "^11.1.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"drizzle-orm": "^0.35.3",
|
"drizzle-orm": "^0.35.3",
|
||||||
|
"hast": "^1.0.0",
|
||||||
|
"lodash.capitalize": "^4.2.1",
|
||||||
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"nuxt": "^3.14.159",
|
"nuxt": "^3.14.159",
|
||||||
"nuxt-security": "^2.0.0",
|
"nuxt-security": "^2.0.0",
|
||||||
"radix-vue": "^1.9.8",
|
"radix-vue": "^1.9.8",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-breaks": "^4.0.0",
|
||||||
|
"remark-frontmatter": "^5.0.0",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
|
"remark-parse": "^11.0.0",
|
||||||
|
"remark-rehype": "^11.1.1",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"unist-util-visit": "^5.0.0",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.1.12",
|
"@types/bun": "^1.1.12",
|
||||||
|
"@types/lodash.capitalize": "^4.2.9",
|
||||||
|
"@types/unist": "^3.0.3",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
"bun-types": "^1.1.34",
|
"bun-types": "^1.1.34",
|
||||||
"drizzle-kit": "^0.26.2"
|
"drizzle-kit": "^0.26.2",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"rehype-stringify": "^10.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,13 @@ async function fetch()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Administration</Title>
|
<Title>d[any] - Administration</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-col justify-start">
|
<div class="flex flex-col justify-start">
|
||||||
<ProseH2>Administration</ProseH2>
|
<ProseH2>Administration</ProseH2>
|
||||||
<Select label="Job" v-model="job">
|
<Select label="Job" v-model="job">
|
||||||
<SelectItem label="Synchroniser" value="sync" />
|
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
|
||||||
<SelectItem label="Nettoyer la base" value="clear" disabled />
|
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
|
||||||
<SelectItem label="Reconstruire" value="rebuild" disabled />
|
|
||||||
</Select>
|
</Select>
|
||||||
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
||||||
<span>Executer</span>
|
<span>Executer</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Editeur</Title>
|
<Title>d[any] - Editeur</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<Editor v-model="model" />
|
<Editor v-model="model" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,6 +42,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||||
|
|
||||||
|
watch(path, () => {
|
||||||
|
if(path.value === 'index')
|
||||||
|
{
|
||||||
|
useRouter().push({ name: 'explore' });
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const { loggedIn, user } = useUserSession();
|
const { loggedIn, user } = useUserSession();
|
||||||
|
|
||||||
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
const { data: page, status, error } = await useFetch(`/api/file/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="page" class="xl:p-12 lg:p-8 py-4 flex flex-1 flex-col items-start justify-start max-h-full">
|
<div v-if="page" class="xl:p-12 lg:p-8 py-4 flex flex-1 flex-col items-start justify-start max-h-full">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Modification de {{ page.title }}</Title>
|
<Title>d[any] - Modification de {{ page.title }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-col xl:flex-row xl:justify-between justify-center items-center w-full px-4 pb-4 border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0">
|
<div class="flex flex-col xl:flex-row xl:justify-between justify-center items-center w-full px-4 pb-4 border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0">
|
||||||
<input type="text" v-model="page.title" placeholder="Titre" class="flex-1 mx-4 h-16 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
|
<input type="text" v-model="page.title" placeholder="Titre" class="flex-1 mx-4 h-16 w-full caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
|
||||||
<div class="flex gap-4 self-end xl:self-auto">
|
<div class="flex gap-4 self-end xl:self-auto flex-wrap">
|
||||||
|
<div class="flex gap-4">
|
||||||
<Tooltip message="Consultable uniquement par le propriétaire" side="bottom"><Switch label="Privé" v-model="page.private" /></Tooltip>
|
<Tooltip message="Consultable uniquement par le propriétaire" side="bottom"><Switch label="Privé" v-model="page.private" /></Tooltip>
|
||||||
<Tooltip message="Afficher dans le menu de navigation" side="bottom"><Switch label="Navigable" v-model="page.navigable" /></Tooltip>
|
<Tooltip message="Afficher dans le menu de navigation" side="bottom"><Switch label="Navigable" v-model="page.navigable" /></Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
<Button @click="() => save()" :loading="saveStatus === 'pending'" class="border-light-blue dark:border-dark-blue hover:border-light-blue dark:hover:border-dark-blue focus:shadow-light-blue dark:focus:shadow-dark-blue">Enregistrer</Button>
|
<Button @click="() => save()" :loading="saveStatus === 'pending'" class="border-light-blue dark:border-dark-blue hover:border-light-blue dark:hover:border-dark-blue focus:shadow-light-blue dark:focus:shadow-dark-blue">Enregistrer</Button>
|
||||||
|
<NuxtLink :href="{ name: 'explore-path', params: { path: path } }"><Button>Annuler</Button></NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-4 flex-1 w-full max-h-full flex">
|
<div class="my-4 flex-1 w-full max-h-full flex">
|
||||||
<template v-if="page.type === 'markdown'">
|
<template v-if="page.type === 'markdown'">
|
||||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" >
|
<SplitterGroup direction="horizontal" class="flex-1 w-full flex" >
|
||||||
<SplitterPanel asChild>
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||||
<textarea v-model="page.content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto"></textarea>
|
<textarea v-model="page.content" class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }"></textarea>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
<SplitterPanel asChild>
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
||||||
<div class="flex-1 max-h-full !overflow-y-auto px-8"><Markdown :content="debounced" :proses="{ 'a': FakeA }" /></div>
|
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><Markdown :content="debounced" :proses="{ 'a': FakeA }" /></div>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</SplitterGroup>
|
</SplitterGroup>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="status === 'pending'" class="flex">
|
<div v-else-if="status === 'pending'" class="flex">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Chargement</Title>
|
<Title>d[any] - Chargement</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
45
pages/explore/edit/index.vue
Normal file
45
pages/explore/edit/index.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { user, loggedIn } = useUserSession();
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
|
const { data: page, status, error } = await useLazyFetch(`/api/navigation`);
|
||||||
|
|
||||||
|
if(!loggedIn || (page.value && page.value.owner !== user.value?.id))
|
||||||
|
{
|
||||||
|
useRouter().replace({ name: 'explore-path', params: { path: path.value } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(): Promise<void>
|
||||||
|
{
|
||||||
|
saveStatus.value = 'pending';
|
||||||
|
try {
|
||||||
|
await $fetch(`/api/file`, {
|
||||||
|
method: 'post',
|
||||||
|
body: page.value,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
saveStatus.value = 'success';
|
||||||
|
|
||||||
|
toaster.clear('error');
|
||||||
|
toaster.add({
|
||||||
|
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
useRouter().push({ name: 'explore-path', params: { path: path.value } });
|
||||||
|
} catch(e: any) {
|
||||||
|
toaster.add({
|
||||||
|
type: 'error', content: e.message, timer: true, duration: 10000
|
||||||
|
})
|
||||||
|
saveStatus.value = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,3 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
Index
|
<div v-if="status === 'pending'" class="flex">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Chargement</Title>
|
||||||
|
</Head>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 justify-start items-start" v-else-if="page">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Accueil</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
|
||||||
|
<div class="flex flex-1 flex-row justify-between items-center">
|
||||||
|
<ProseH1>{{ page.title }}</ProseH1>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<NuxtLink :href="{ name: 'explore-edit-path', params: { path: 'index' } }"><Button v-if="isOwner">Modifier la page</Button></NuxtLink>
|
||||||
|
<NuxtLink :href="{ name: 'explore-edit' }"><Button v-if="isOwner">Configurer le projet</Button></NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Markdown :content="page.content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="status === 'error'">
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Erreur</Title>
|
||||||
|
</Head>
|
||||||
|
<span>{{ error?.message }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Erreur</Title>
|
||||||
|
</Head>
|
||||||
|
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { loggedIn, user } = useUserSession();
|
||||||
|
|
||||||
|
const { data: page, status, error } = await useFetch(`/api/file/index`);
|
||||||
|
const isOwner = computed(() => user.value?.id === page.value?.owner);
|
||||||
|
</script>
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const open = ref(false), username = ref(""), price = ref(750), disabled = ref(false), loading = ref(false);
|
|
||||||
|
|
||||||
watch(loading, (value) => {
|
|
||||||
if(value)
|
|
||||||
{
|
|
||||||
setTimeout(() => { open.value = true; loading.value = false }, 1500);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Accueil</Title>
|
<Title>d[any] - Accueil</Title>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Mentions légales</Title>
|
||||||
|
</Head>
|
||||||
<div class="flex flex-col max-w-[1200px] p-16">
|
<div class="flex flex-col max-w-[1200px] p-16">
|
||||||
<ProseH3>Mentions Légales</ProseH3>
|
<ProseH3>Mentions Légales</ProseH3>
|
||||||
<ProseH4>Collecte et Traitement des Données Personnelles</ProseH4>
|
<ProseH4>Collecte et Traitement des Données Personnelles</ProseH4>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Connexion</Title>
|
<Title>d[any] - Connexion</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { hasPermissions } from "#shared/auth.util";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
guestsGoesTo: '/user/login',
|
guestsGoesTo: '/user/login',
|
||||||
})
|
})
|
||||||
@@ -16,7 +18,7 @@ async function deleteUser()
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Mon profil</Title>
|
<Title>d[any] - Mon profil</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="grid lg:grid-cols-4 grid-col-2 w-full items-start py-8 gap-6 content-start" v-if="user">
|
<div class="grid lg:grid-cols-4 grid-col-2 w-full items-start py-8 gap-6 content-start" v-if="user">
|
||||||
<div class="flex flex-col gap-4 col-span-4 lg:col-span-3 border border-light-35 dark:border-dark-35 p-4">
|
<div class="flex flex-col gap-4 col-span-4 lg:col-span-3 border border-light-35 dark:border-dark-35 p-4">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Head>
|
<Head>
|
||||||
<Title>Inscription</Title>
|
<Title>d[any] - Inscription</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-1 flex-col justify-center items-center">
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
<div class="flex gap-8 items-center">
|
<div class="flex gap-8 items-center">
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const schema = z.object({
|
export const schema = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
owner: z.number(),
|
owner: z.number().finite(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
navigable: z.boolean(),
|
navigable: z.boolean(),
|
||||||
private: z.boolean(),
|
private: z.boolean(),
|
||||||
|
order: z.number().finite(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type File = z.infer<typeof schema>;
|
export type File = z.infer<typeof schema>;
|
||||||
15
schemas/navigation.ts
Normal file
15
schemas/navigation.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const single = z.object({
|
||||||
|
path: z.string(),
|
||||||
|
owner: z.number().finite(),
|
||||||
|
title: z.string(),
|
||||||
|
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
|
||||||
|
navigable: z.boolean(),
|
||||||
|
private: z.boolean(),
|
||||||
|
order: z.number().finite(),
|
||||||
|
});
|
||||||
|
export const table = z.array(single);
|
||||||
|
|
||||||
|
export type Navigation = z.infer<typeof table>;
|
||||||
|
export type NavigationItem = z.infer<typeof single>;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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,6 +1,6 @@
|
|||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { explorerContentTable } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
import type { Navigation } from '~/types/api';
|
import type { Navigation, NavigationItem } from '~/schemas/navigation';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const { user } = await getUserSession(e);
|
const { user } = await getUserSession(e);
|
||||||
@@ -11,11 +11,19 @@ export default defineEventHandler(async (e) => {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const content = db.select({ path: explorerContentTable.path, title: explorerContentTable.title, type: explorerContentTable.type, private: explorerContentTable.private, navigable: explorerContentTable.navigable, owner: explorerContentTable.owner }).from(explorerContentTable).prepare().all() as (Navigation & { owner: number, navigable: boolean })[];
|
const content = db.select({
|
||||||
|
path: explorerContentTable.path,
|
||||||
|
type: explorerContentTable.type,
|
||||||
|
owner: explorerContentTable.owner,
|
||||||
|
title: explorerContentTable.title,
|
||||||
|
navigable: explorerContentTable.navigable,
|
||||||
|
private: explorerContentTable.private,
|
||||||
|
order: explorerContentTable.order,
|
||||||
|
}).from(explorerContentTable).prepare().all();
|
||||||
|
|
||||||
if(content.length > 0)
|
if(content.length > 0)
|
||||||
{
|
{
|
||||||
const navigation: Navigation[] = [];
|
const navigation: Navigation = [];
|
||||||
|
|
||||||
for(const idx in content)
|
for(const idx in content)
|
||||||
{
|
{
|
||||||
@@ -45,7 +53,7 @@ export default defineEventHandler(async (e) => {
|
|||||||
setResponseStatus(e, 404);
|
setResponseStatus(e, 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
function addChild(arr: Navigation[], e: Navigation): void
|
function addChild(arr: Navigation, e: NavigationItem): void
|
||||||
{
|
{
|
||||||
const parent = arr.find(f => e.path.startsWith(f.path));
|
const parent = arr.find(f => e.path.startsWith(f.path));
|
||||||
|
|
||||||
@@ -58,6 +66,11 @@ function addChild(arr: Navigation[], e: Navigation): void
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
arr.push({ title: e.title, path: e.path, type: e.type, private: e.private });
|
arr.push({ title: e.title, path: e.path, type: e.type, order: e.order, private: e.private });
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
if(a.order && b.order)
|
||||||
|
return a.order - b.order;
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
49
server/api/navigation.post.ts
Normal file
49
server/api/navigation.post.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { hasPermissions } from "#shared/auth.util";
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { explorerContentTable } from '~/db/schema';
|
||||||
|
import { schema } from '~/schemas/navigation';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
const { user } = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!user || !hasPermissions(user.permissions, ['admin', 'editor']))
|
||||||
|
{
|
||||||
|
throw createError({ statusCode: 401, statusText: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
{
|
||||||
|
throw body.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
db.transaction((tx) => {
|
||||||
|
for(let i = 0; i < body.data.length; i++)
|
||||||
|
{
|
||||||
|
tx.insert(explorerContentTable).values({
|
||||||
|
path: body.data[i].path,
|
||||||
|
owner: body.data[i].owner,
|
||||||
|
title: body.data[i].title,
|
||||||
|
type: body.data[i].type,
|
||||||
|
navigable: body.data[i].navigable,
|
||||||
|
private: body.data[i].private,
|
||||||
|
order: body.data[i].order,
|
||||||
|
content: Buffer.from('', 'utf-8'),
|
||||||
|
}).onConflictDoUpdate({
|
||||||
|
set: {
|
||||||
|
owner: body.data[i].owner,
|
||||||
|
title: body.data[i].title,
|
||||||
|
type: body.data[i].type,
|
||||||
|
navigable: body.data[i].navigable,
|
||||||
|
private: body.data[i].private,
|
||||||
|
order: body.data[i].order,
|
||||||
|
},
|
||||||
|
target: explorerContentTable.path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setResponseStatus(e, 404);
|
||||||
|
});
|
||||||
41
server/tasks/pull.ts
Normal file
41
server/tasks/pull.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import useDatabase from "~/composables/useDatabase";
|
||||||
|
import type { FileType } from '~/types/api';
|
||||||
|
import { explorerContentTable } from "~/db/schema";
|
||||||
|
import { eq, ne } from "drizzle-orm";
|
||||||
|
|
||||||
|
const typeMapping: Record<string, FileType> = {
|
||||||
|
".md": "markdown",
|
||||||
|
".canvas": "canvas"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineTask({
|
||||||
|
meta: {
|
||||||
|
name: 'pull',
|
||||||
|
description: 'Pull the data from Git',
|
||||||
|
},
|
||||||
|
async run(event) {
|
||||||
|
try {
|
||||||
|
const tree = await $fetch('https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/git/trees/master', {
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json',
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
recursive: true,
|
||||||
|
per_page: 1000,
|
||||||
|
}
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
|
||||||
|
|
||||||
|
useStorage('cache').clear();
|
||||||
|
|
||||||
|
return { result: true };
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
return { result: false, error: e };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -11,8 +11,8 @@ const typeMapping: Record<string, FileType> = {
|
|||||||
|
|
||||||
export default defineTask({
|
export default defineTask({
|
||||||
meta: {
|
meta: {
|
||||||
name: 'sync',
|
name: 'push',
|
||||||
description: 'Synchronise the project with Obsidian',
|
description: 'Push the data to Git',
|
||||||
},
|
},
|
||||||
async run(event) {
|
async run(event) {
|
||||||
try {
|
try {
|
||||||
@@ -27,7 +27,6 @@ export default defineTask({
|
|||||||
}
|
}
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
|
|
||||||
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
|
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
|
||||||
if(e.type === 'tree')
|
if(e.type === 'tree')
|
||||||
{
|
{
|
||||||
@@ -64,71 +63,10 @@ export default defineTask({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/*let tags: Tag[] = [];
|
|
||||||
const tagFile = files.find(e => e.path === "tags");
|
|
||||||
|
|
||||||
if(tagFile)
|
|
||||||
{
|
|
||||||
const parsed = useMarkdown()(tagFile.content);
|
|
||||||
const titles = parsed.children.filter(e => e.type === 'element' && e.tagName.match(/h\d/));
|
|
||||||
for(let i = 0; i < titles.length; i++)
|
|
||||||
{
|
|
||||||
const start = titles[i].position?.start.offset ?? 0;
|
|
||||||
const end = titles.length === i + 1 ? tagFile.content.length : titles[i + 1].position.start.offset - 1;
|
|
||||||
tags.push({ tag: titles[i].properties.id, description: tagFile.content.substring(titles[i].position?.end.offset + 1, end) });
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
db.delete(explorerContentTable).run();
|
db.delete(explorerContentTable).run();
|
||||||
db.insert(explorerContentTable).values(files).run();
|
db.insert(explorerContentTable).values(files).run();
|
||||||
|
|
||||||
/*const oldFiles = db.prepare(`SELECT * FROM explorer_files WHERE project = ?1`).all('1') as File[];
|
|
||||||
const removeFiles = db.prepare(`DELETE FROM explorer_files WHERE project = ?1 AND path = ?2`);
|
|
||||||
db.transaction((data: File[]) => data.forEach(e => removeFiles.run('1', e.path)))(oldFiles.filter(e => !files.find(f => f.path = e.path)));
|
|
||||||
removeFiles.finalize();
|
|
||||||
|
|
||||||
const oldTags = db.prepare(`SELECT * FROM explorer_tags WHERE project = ?1`).all('1') as Tag[];
|
|
||||||
const removeTags = db.prepare(`DELETE FROM explorer_tags WHERE project = ?1 AND tag = ?2`);
|
|
||||||
db.transaction((data: Tag[]) => data.forEach(e => removeTags.run('1', e.tag)))(oldTags.filter(e => !tags.find(f => f.tag = e.tag)));
|
|
||||||
removeTags.finalize();
|
|
||||||
|
|
||||||
const insertFiles = db.prepare(`INSERT INTO explorer_files("project", "path", "owner", "title", "order", "type", "content") VALUES (1, $path, 1, $title, $order, $type, $content)`);
|
|
||||||
const updateFiles = db.prepare(`UPDATE explorer_files SET content = $content WHERE project = 1 AND path = $path`);
|
|
||||||
db.transaction((content) => {
|
|
||||||
for (const item of content) {
|
|
||||||
let order = item.order;
|
|
||||||
|
|
||||||
if (typeof order === 'string')
|
|
||||||
order = parseInt(item.order, 10);
|
|
||||||
|
|
||||||
if (isNaN(order))
|
|
||||||
order = 999;
|
|
||||||
|
|
||||||
if(oldFiles.find(e => item.path === e.path))
|
|
||||||
updateFiles.run({ $path: item.path, $content: item.content });
|
|
||||||
else
|
|
||||||
insertFiles.run({ $path: item.path, $title: item.title, $type: item.type, $content: item.content, $order: order });
|
|
||||||
}
|
|
||||||
})(files);
|
|
||||||
|
|
||||||
insertFiles.finalize();
|
|
||||||
updateFiles.finalize();
|
|
||||||
|
|
||||||
const insertTags = db.prepare(`INSERT INTO explorer_tags("project", "tag", "description") VALUES (1, $tag, $description)`);
|
|
||||||
const updateTags = db.prepare(`UPDATE explorer_tags SET description = $description WHERE project = 1 AND tag = $tag`);
|
|
||||||
db.transaction((content) => {
|
|
||||||
for (const item of content) {
|
|
||||||
if (oldTags.find(e => item.tag === e.tag))
|
|
||||||
updateTags.run({ $tag: item.tag, $description: item.description });
|
|
||||||
else
|
|
||||||
insertTags.run({ $tag: item.tag, $description: item.description });
|
|
||||||
}
|
|
||||||
})(tags);
|
|
||||||
|
|
||||||
insertTags.finalize();
|
|
||||||
updateTags.finalize();*/
|
|
||||||
|
|
||||||
useStorage('cache').clear();
|
useStorage('cache').clear();
|
||||||
|
|
||||||
return { result: true };
|
return { result: true };
|
||||||
Reference in New Issue
Block a user