New Canvas zoom with dbl click and starting to work on tag API

This commit is contained in:
Peaceultime 2024-09-03 16:42:57 +02:00
parent c091a6d261
commit 2b293a0c1a
16 changed files with 353 additions and 77 deletions

View File

@ -10,11 +10,26 @@ const props = defineProps<Props>();
let dragging = false, posX = 0, posY = 0, dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
let lastDistance = 0;
let lastClickTime = 0;
const onPointerDown = (event: PointerEvent) => {
if (event.isPrimary === false) return;
dragging = true;
const now = performance.now();
if(now - lastClickTime < 500 && Math.abs(event.clientX - posX) < 20 && Math.abs(event.clientY - posY) < 20)
{
if(event.ctrlKey)
{
zoom.value = clamp(zoom.value * 0.9, minZoom.value, 3);
}
else
{
zoom.value = clamp(zoom.value * 1.1, minZoom.value, 3);
}
}
lastClickTime = now;
posX = event.clientX;
posY = event.clientY;

View File

@ -1,15 +1,15 @@
<template>
<Teleport to="#teleports" v-if="display && (!fetched || loaded)">
<div class="absolute border-2 border-light-35 dark:border-dark-35 max-w-[550px] max-h-[450px] bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100" :class="[{'is-loaded': fetched}, type === 'Markdown' ? 'overflow-auto' : 'overflow-hidden']" :style="pos"
<div class="absolute border-2 border-light-35 dark:border-dark-35 max-w-[550px] max-h-[450px] bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100" :class="[{'is-loaded': fetched}, file?.type === 'Markdown' ? 'overflow-auto' : 'overflow-hidden']" :style="pos"
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => display = false, 250)">
<div v-if="pending" class="loading"></div>
<template v-else-if="content !==''">
<div v-if="type === 'Markdown'" class="p-6 ms-6">
<ProseH1>{{ title }}</ProseH1>
<Markdown v-model="content"></Markdown>
<template v-else-if="!!file">
<div v-if="file.type === 'Markdown'" class="p-6 ms-6">
<ProseH1>{{ file.title }}</ProseH1>
<Markdown v-model="file.content"></Markdown>
</div>
<div v-else-if="type === 'Canvas'" class="w-[550px] h-[450px] overflow-hidden">
<CanvasRenderer :canvas="JSON.parse(content) " />
<div v-else-if="file.type === 'Canvas'" class="w-[550px] h-[450px] overflow-hidden">
<CanvasRenderer :canvas="JSON.parse(file.content) " />
</div>
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
@ -28,6 +28,7 @@
</template>
<script setup lang="ts">
import type { CommentedFile } from '~/types/api';
const props = defineProps({
project: {
@ -43,30 +44,20 @@ const props = defineProps({
required: false,
}
})
const content = ref(''), title = ref(''), type = ref(''), display = ref(false), pending = ref(false), fetched = ref(false);
const file = ref<CommentedFile | null>();
const display = ref(false), pending = ref(false), fetched = ref(false);
const el = ref(), pos = ref<Record<string, string>>();
const loaded = computed(() => !pending.value && fetched.value && content.value !== '');
const loaded = computed(() => !pending.value && fetched.value && file.value !== undefined);
async function fetch()
{
fetched.value = true;
pending.value = true;
const data = await $fetch(`/api/project/${props.project}/file`, {
method: 'get',
query: {
path: props.path,
},
});
const { data } = await useFetch(`/api/project/${props.project}/file/${encodeURIComponent(props.path)}`);
pending.value = false;
if(data && data[0])
{
content.value = data[0].content;
title.value = data[0].title;
type.value = data[0].type;
}
file.value = data.value;
}
async function show()
{

View File

@ -41,7 +41,8 @@ const { data, status } = await useFetch(`/api/project/${project.value}/file`, {
},
transform: (data) => data?.map(e => ({ path: e.path, type: e.type })),
key: `file:${project.value}:%${pathname}`,
dedupe: 'defer'
dedupe: 'defer',
ignoreResponseError: true,
});
</script>

View File

@ -1,12 +1,182 @@
<template>
<span v-if="focused">> </span>
<blockquote ref="el" @focusin="focused = true" @focusout="focused = false">
<slot />
</blockquote>
<blockquote ref="el">
<slot />
</blockquote>
</template>
<script setup lang="ts">
const focused = ref(false);
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
watch(focused, console.log);
onMounted(() => {
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
{
title.value = el.value.querySelector('.callout-title');
title.value?.addEventListener('click', toggle);
}
});
onUnmounted(() => {
title.value?.removeEventListener('click', toggle);
})
function toggle() {
el.value?.classList?.toggle('is-collapsed');
}
</script>
<style>
blockquote:not(.callout)
{
@apply ps-4;
@apply my-4;
@apply relative;
@apply before:absolute;
@apply before:-top-1;
@apply before:-bottom-1;
@apply before:left-0;
@apply before:w-1;
@apply before:bg-light-30;
@apply dark:before:bg-dark-30;
}
blockquote:empty
{
@apply before:hidden;
}
.callout {
@apply bg-light-blue;
@apply dark:bg-dark-blue;
}
.callout.is-collapsible .callout-title
{
@apply cursor-pointer;
}
.callout .fold
{
@apply transition-transform;
}
.callout.is-collapsed .fold
{
@apply -rotate-90;
}
.callout.is-collapsed > p
{
@apply hidden;
}
.callout[datacallout="abstract"],
.callout[datacallout="summary"],
.callout[datacallout="tldr"] {
@apply bg-light-cyan;
@apply dark:bg-dark-cyan;
@apply text-light-cyan;
@apply dark:text-dark-cyan;
}
.callout[datacallout="info"] {
@apply bg-light-blue;
@apply dark:bg-dark-blue;
@apply text-light-blue;
@apply dark:text-dark-blue;
}
.callout[datacallout="todo"] {
@apply bg-light-blue;
@apply dark:bg-dark-blue;
@apply text-light-blue;
@apply dark:text-dark-blue;
}
.callout[datacallout="important"] {
@apply bg-light-cyan;
@apply dark:bg-dark-cyan;
@apply text-light-cyan;
@apply dark:text-dark-cyan;
}
.callout[datacallout="tip"],
.callout[datacallout="hint"] {
@apply bg-light-cyan;
@apply dark:bg-dark-cyan;
@apply text-light-cyan;
@apply dark:text-dark-cyan;
}
.callout[datacallout="success"],
.callout[datacallout="check"],
.callout[datacallout="done"] {
@apply bg-light-green;
@apply dark:bg-dark-green;
@apply text-light-green;
@apply dark:text-dark-green;
}
.callout[datacallout="question"],
.callout[datacallout="help"],
.callout[datacallout="faq"] {
@apply bg-light-orange;
@apply dark:bg-dark-orange;
@apply text-light-orange;
@apply dark:text-dark-orange;
}
.callout[datacallout="warning"],
.callout[datacallout="caution"],
.callout[datacallout="attention"] {
@apply bg-light-orange;
@apply dark:bg-dark-orange;
@apply text-light-orange;
@apply dark:text-dark-orange;
}
.callout[datacallout="failure"],
.callout[datacallout="fail"],
.callout[datacallout="missing"] {
@apply bg-light-red;
@apply dark:bg-dark-red;
@apply text-light-red;
@apply dark:text-dark-red;
}
.callout[datacallout="danger"],
.callout[datacallout="error"] {
@apply bg-light-red;
@apply dark:bg-dark-red;
@apply text-light-red;
@apply dark:text-dark-red;
}
.callout[datacallout="bug"] {
@apply bg-light-red;
@apply dark:bg-dark-red;
@apply text-light-red;
@apply dark:text-dark-red;
}
.callout[datacallout="example"] {
@apply bg-light-purple;
@apply dark:bg-dark-purple;
@apply text-light-purple;
@apply dark:text-dark-purple;
}
.callout
{
@apply overflow-hidden;
@apply my-4;
@apply p-3;
@apply ps-6;
@apply bg-blend-lighten;
@apply !bg-opacity-25;
@apply border-l-4;
@apply inline-block;
@apply pe-8;
}
.callout-icon
{
@apply w-6;
@apply h-6;
@apply stroke-2;
}
.callout-title
{
@apply flex;
@apply items-center;
@apply gap-2;
}
.callout-title-inner
{
@apply inline-block;
@apply font-bold;
}
.callout > p
{
@apply mt-2;
@apply font-semibold;
}
</style>

View File

@ -1,3 +1,9 @@
<template>
<span v-show="false"># </span><h1><slot></slot></h1>
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2 relative sm:right-8 right-4">
<slot />
</h1>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>

View File

@ -1,12 +1,11 @@
<template>
<span v-show="focused">## </span><h2><slot></slot></h2>
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-8 right-4">
<slot />
</h2>
</template>
<script setup>
const props = defineProps({
focused: {
type: Boolean,
default: false
}
})
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>

View File

@ -1,3 +1,11 @@
<template>
<span v-show="false">### </span><h3><slot></slot></h3>
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
<slot />
</h3>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>

View File

@ -1,3 +1,9 @@
<template>
<span v-show="false">#### </span><h4><slot></slot></h4>
<h4 :id="parseId(id)" class="text-xl font-semibold my-2" style="font-variant: small-caps;">
<slot />
</h4>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>

View File

@ -1,3 +1,11 @@
<template>
<span v-show="false">##### </span><h5><slot></slot></h5>
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
<slot />
</h5>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>

View File

@ -1,3 +1,11 @@
<template>
<span v-show="false">###### </span><h6><slot></slot></h6>
<h6 :id="parseId(id)">
<slot />
</h6>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>

BIN
db.sqlite

Binary file not shown.

View File

@ -36,7 +36,7 @@ function toggleNavigation(bool?: boolean)
<p>Copyright Peaceultime - 2024</p>
</div>
</div>
<div class="flex-1 flex items-baseline overflow-auto p-8 relative">
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
<slot></slot>
</div>
</div>

View File

@ -1,27 +0,0 @@
import useDatabase from '~/composables/useDatabase';
import { Comment } from '~/types/api';
export default defineEventHandler(async (e) => {
const project = getRouterParam(e, "projectId");
const query = getQuery(e);
if(!project || !query.path)
{
setResponseStatus(e, 404);
return;
}
const where = ["project = $project", "path = $path"];
const criteria: Record<string, any> = { $project: project, $path: query.path };
if(where.length > 1)
{
const db = useDatabase();
const content = db.query(`SELECT * FROM explorer_comments WHERE ${where.join(" and ")}`).all(criteria) as Comment[];
return content;
}
setResponseStatus(e, 404);
});

View File

@ -0,0 +1,41 @@
import useDatabase from '~/composables/useDatabase';
import type { CommentedFile, CommentSearch, File } from '~/types/api';
export default defineCachedEventHandler(async (e) => {
const project = getRouterParam(e, "projectId");
const path = decodeURIComponent(getRouterParam(e, "path") ?? '');
if(!project)
{
setResponseStatus(e, 404);
return;
}
if(!path)
{
setResponseStatus(e, 404);
return;
}
const where = ["project = $project", "path = $path"];
const criteria: Record<string, any> = { $project: project, $path: path };
if(where.length > 1)
{
const db = useDatabase();
const content = db.query(`SELECT * FROM explorer_files WHERE ${where.join(" and ")}`).get(criteria) as File;
if(content !== undefined)
{
const comments = db.query(`SELECT comment.*, user.username FROM explorer_comments comment LEFT JOIN users user ON comment.user_id = user.id WHERE ${where.join(" and ")}`).all(criteria) as CommentSearch[];
return { ...content, comments } as CommentedFile;
}
}
setResponseStatus(e, 404);
return;
}, {
maxAge: 60*60*24,
getKey: (e) => `file-${getRouterParam(e, "projectId")}-${getRouterParam(e, "path")}`
});

View File

@ -0,0 +1,39 @@
import useDatabase from '~/composables/useDatabase';
import type { Tag } from '~/types/api';
export default defineCachedEventHandler(async (e) => {
const project = getRouterParam(e, "projectId");
const tag = getRouterParam(e, "tag");
const query = getQuery(e);
if(!project)
{
setResponseStatus(e, 404);
return;
}
if(!tag)
{
setResponseStatus(e, 404);
return;
}
const where = ["project = $project", "tag = $tag"];
const criteria: Record<string, any> = { $project: project, $tag: tag };
if(where.length > 1)
{
const db = useDatabase();
const content = db.query(`SELECT * FROM explorer_tags WHERE ${where.join(" and ")}`).get(criteria) as Tag;
if(content !== undefined)
{
return content;
}
}
setResponseStatus(e, 404);
}, {
maxAge: 60*60*24,
getKey: (e) => `tag-${getRouterParam(e, "projectId")}-${getRouterParam(e, "tag")}`
});

11
types/api.d.ts vendored
View File

@ -52,6 +52,13 @@ export interface User {
id: number;
username: string;
}
export interface Tag {
tag: string;
project: number;
description: string;
}
export type ProjectSearch = Project &
{
pages: number;
@ -69,6 +76,10 @@ export type CommentSearch = Comment &
export type UserSearch = User &
{
}
export type CommentedFile = File &
{
comments: CommentSearch[];
}
export interface Search {
projects: ProjectSearch[];
files: FileSearch[];