zwischenstand

This commit is contained in:
Martin Drechsel
2025-05-06 11:09:56 +02:00
parent 410a885d21
commit b729c8ebbe
40 changed files with 2252 additions and 376 deletions

View File

@ -0,0 +1,102 @@
<template>
<UiDialog :title="t('title')" v-model:open="open">
<div>
<i18n-t keypath="question" tag="p">
<template #extension>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
</i18n-t>
<!-- {{ t("question", { extension: manifest?.name }) }}
<span class="font-bold text-primary">{{ manifest?.name }}</span> zu HaexHub hinzufügen? -->
</div>
<div class="flex flex-col">
<HaexExtensionManifestPermissionsFilesystem
v-if="manifest?.permissions?.filesystem"
:filesystem="manifest?.permissions?.filesystem"
/>
<HaexExtensionManifestPermissionsDatabase
v-if="manifest?.permissions?.database"
:database="manifest?.permissions?.database"
/>
<HaexExtensionManifestPermissionsHttp
v-if="manifest?.permissions?.http"
:http="manifest?.permissions?.http"
/>
<!-- <VaultCard>
<template #header>
<h3>{{ t("filesystem.title") }}</h3>
</template>
<div>
{{ manifest?.permissions.filesystem }}
</div>
</VaultCard>
<VaultCard>
<template #header>
<h3>{{ t("http.title") }}</h3>
</template>
<div>
{{ manifest?.permissions.http }}
</div>
</VaultCard> -->
</div>
<template #buttons>
<UiButton @click="onDeny" class="btn-error btn-outline">{{ t("deny") }} </UiButton>
<UiButton @click="onConfirm" class="btn-success btn-outline">{{ t("confirm") }}</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
defineProps<{ manifest?: IHaexHubExtensionManifest }>();
const emit = defineEmits(["deny", "confirm"]);
const onDeny = () => {
open.value = false;
console.log("onDeny open", open.value);
emit("deny");
};
const onConfirm = () => {
open.value = false;
console.log("onConfirm open", open.value);
emit("confirm");
};
</script>
<i18n lang="json">
{
"de": {
"title": "Erweiterung hinzufügen",
"question": "Möchtest du die Erweiterung {extension} hinzufügen?",
"confirm": "Bestätigen",
"deny": "Ablehnen",
"permission": {
"read": "Lesen",
"write": "Schreiben"
},
"database": {
"title": "Datenbank Berechtigungen"
},
"http": {
"title": "Internet Berechtigungen"
},
"filesystem": {
"title": "Dateisystem Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
</i18n>

View File

@ -0,0 +1,63 @@
<template>
<div>
<HaexExtensionManifestPermissionsTitle>
{{ t("database.title") }}
</HaexExtensionManifestPermissionsTitle>
<div v-if="database?.read?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.read") }}</h3>
</template>
<ul class="space-y-0.5">
<li class="flex items-center justify-between px-4 py-0.5" v-for="read in database?.read">
<div class="flex items-center gap-2">
<span>{{ read }}</span>
</div>
</li>
</ul>
</UiAccordion>
</div>
<div v-if="database?.write?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.write") }}</h3>
</template>
<ul class="space-y-0.5">
<li
class="flex items-center justify-between px-4 py-0.5"
v-for="write in database?.write"
>
<div class="flex items-center gap-2">
<span>{{ write }}</span>
</div>
</li>
</ul>
</UiAccordion>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{ database: IHaexHubExtensionManifest["permissions"]["database"] }>();
const { t } = useI18n();
</script>
<i18n lang="json">
{
"de": {
"permission": {
"read": "Lesen",
"write": "Schreiben"
},
"database": {
"title": "Datenbank Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
</i18n>

View File

@ -0,0 +1,65 @@
<template>
<div>
<HaexExtensionManifestPermissionsTitle>
{{ t("filesystem.title") }}
</HaexExtensionManifestPermissionsTitle>
<div v-if="filesystem?.read?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.read") }}</h3>
</template>
<ul class="space-y-0.5">
<li
class="flex items-center justify-between px-4 py-0.5"
v-for="read in filesystem?.read"
>
<div class="flex items-center gap-2">
<span>{{ read }}</span>
</div>
</li>
</ul>
</UiAccordion>
</div>
<div v-if="filesystem?.write?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.write") }}</h3>
</template>
<ul class="space-y-0.5">
<li
class="flex items-center justify-between px-4 py-0.5"
v-for="write in filesystem?.write"
>
<div class="flex items-center gap-2">
<span>{{ write }}</span>
</div>
</li>
</ul>
</UiAccordion>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{ filesystem: IHaexHubExtensionManifest["permissions"]["filesystem"] }>();
const { t } = useI18n();
</script>
<i18n lang="json">
{
"de": {
"permission": {
"read": "Lesen",
"write": "Schreiben"
},
"filesystem": {
"title": "Dateisystem Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
</i18n>

View File

@ -0,0 +1,43 @@
<template>
<div>
<HaexExtensionManifestPermissionsTitle>
{{ t("http.title") }}
</HaexExtensionManifestPermissionsTitle>
<div v-if="http?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.access") }}</h3>
</template>
<ul class="space-y-0.5">
<li class="flex items-center justify-between px-4 py-0.5" v-for="access in http">
<div class="flex items-center gap-2">
<span>{{ access }}</span>
</div>
</li>
</ul>
</UiAccordion>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{ http: IHaexHubExtensionManifest["permissions"]["http"] }>();
const { t } = useI18n();
</script>
<i18n lang="json">
{
"de": {
"permission": {
"access": "Zugriff"
},
"http": {
"title": "Internet Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
</i18n>

View File

@ -0,0 +1,5 @@
<template>
<div class="text-base-content/50 px-4 py-2 text-md font-medium">
<slot />
</div>
</template>

View File

@ -0,0 +1,43 @@
<template>
<div class="accordion divide-neutral/20 divide-y">
<div class="accordion-item active" :id="itemId" ref="accordionRef">
<button
class="accordion-toggle inline-flex items-center gap-x-4 text-start"
:aria-controls="collapseId"
aria-expanded="true"
type="button"
>
<span
class="icon-[tabler--chevron-right] accordion-item-active:rotate-90 size-5 shrink-0 transition-transform duration-300 rtl:rotate-180"
></span>
<slot name="title" />
</button>
<div
:id="collapseId"
class="accordion-content w-full overflow-hidden transition-[height] duration-300"
:aria-labelledby="itemId"
role="region"
>
<div class="px-5 pb-4">
<slot />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { HSAccordion } from "flyonui/flyonui";
const itemId = useId();
const collapseId = useId();
const accordionRef = useTemplateRef("accordionRef");
const accordion = ref<HSAccordion>();
onMounted(() => {
if (accordionRef.value) {
accordion.value = new HSAccordion(accordionRef.value);
}
});
</script>

View File

@ -1,9 +1,5 @@
<template>
<slot
name="trigger"
:id
>
</slot>
<slot name="trigger" :id> </slot>
<div
:id
@ -17,10 +13,7 @@
<div class="modal-content">
<div class="modal-header">
<slot name="title">
<h3
v-if="title"
class="modal-title text-base sm:text-lg"
>
<h3 v-if="title" class="modal-title text-base sm:text-lg">
{{ title }}
</h3>
</slot>
@ -32,10 +25,7 @@
@click="open = false"
tabindex="1"
>
<Icon
name="mdi:close"
size="18"
/>
<Icon name="mdi:close" size="18" />
</button>
</div>
<div class="modal-body text-sm sm:text-base py-1">
@ -50,7 +40,7 @@
</template>
<script setup lang="ts">
import { HSOverlay } from 'flyonui/flyonui';
import { HSOverlay } from "flyonui/flyonui";
export interface IDom {
class?: String;
@ -63,38 +53,39 @@ defineProps({
trigger: {
type: Object as PropType<IDom>,
default: () => ({
class: '',
text: '',
class: "",
text: "",
}),
},
title: {
type: String,
default: '',
default: "",
},
description: {
type: Object as PropType<IDom>,
default: () => ({
class: '',
text: '',
class: "",
text: "",
}),
required: false,
},
});
const open = defineModel<boolean>('open', { default: false });
const open = defineModel<boolean>("open", { default: false });
const { t } = useI18n();
const modalRef = useTemplateRef('modalRef');
const modalRef = useTemplateRef("modalRef");
const modal = ref<HSOverlay>();
watch(open, async () => {
console.log("open modal", open.value);
if (open.value) {
//console.log('open modal', modal.value?.open);
await modal.value?.open();
} else {
await modal.value?.close(true);
const res = await modal.value?.close(true);
console.log("close dialog", res);
}
});
@ -102,8 +93,8 @@ onMounted(() => {
if (!modalRef.value) return;
modal.value = new HSOverlay(modalRef.value, { isClosePrev: true });
modal.value.on('close', () => {
console.log('close it from event', open.value);
modal.value.on("close", () => {
console.log("close it from event", open.value);
open.value = false;
});
});

View File

@ -0,0 +1,49 @@
<template>
<li
@click="triggerNavigate"
class="hover:text-primary rounded"
:class="{ ['bg-base-300']: isActive }"
>
<UiTooltip :tooltip="tooltip ?? name" direction="right-start">
<NuxtLinkLocale
:to="{ name: 'haexExtension', params: { extensionId: props.id } }"
class="flex items-center justify-center cursor-pointer tooltip-toogle"
ref="link"
>
<div v-html="icon" class="shrink-0 size-6" />
<!-- <Icon mode="svg" :name="icon" class="shrink-0 size-6" /> -->
</NuxtLinkLocale>
</UiTooltip>
</li>
</template>
<script setup lang="ts">
import { type ISidebarItem } from "#imports";
const props = defineProps<ISidebarItem>();
console.log("image", props.icon);
const router = useRouter();
const isActive = computed(() => {
if (props.to?.name === "haexExtension") {
return getSingleRouteParam(router.currentRoute.value.params.extensionId) === props.id;
} else {
return props.to?.name === router.currentRoute.value.meta.name;
}
});
const link = useTemplateRef("link");
const triggerNavigate = () => link.value?.$el.click();
/* computed(() => {
const found = useRouter()
.getRoutes()
.find((route) => route.name === useLocaleRoute()(props.to)?.name);
console.log('found route', found, useRoute());
return (
found?.name === useRoute().name ||
found?.children.some((child) => child.name === useRoute().name)
);
}); */
</script>

View File

@ -71,7 +71,8 @@ const { add } = useSnackbar();
const handleError = (error: unknown) => {
isOpen.value = false;
add({ type: "error", text: JSON.stringify(error) });
console.log("handleError", error, typeof error);
add({ type: "error", text: "Passwort falsch" });
//console.error(error);
};
@ -100,6 +101,7 @@ const onLoadDatabase = async () => {
};
const localePath = useLocalePath();
const onOpenDatabase = async () => {
try {
check.value = true;
@ -120,7 +122,10 @@ const onOpenDatabase = async () => {
});
if (!vaultId) {
add({ type: "error", text: "Vault konnte nicht geöffnet werden" });
add({
type: "error",
text: "Vault konnte nicht geöffnet werden. \n Vermutlich ist das Passwort falsch",
});
return;
}

View File

@ -1,14 +1,27 @@
<template>
<div
class="bg-base-100 w-full mx-auto shadow h-full overflow-hidden pt-[7.5rem]"
>
<div class="card">
<slot name="image" />
<div class="card-header">
<h5 class="card-title" v-if="$slots.title">
<slot name="title" />
</h5>
</div>
<div class="card-body">
<slot />
<div class="card-actions" v-if="$slots.action">
<slot name="action" />
</div>
</div>
</div>
<!-- <div class="bg-base-100 w-full mx-auto shadow h-full overflow-hidden pt-[7.5rem]">
<div
class="fixed top-0 right-0 z-10 transition-all duration-700 w-full font-semibold text-lg h-[7.5rem]"
:class="{ 'pl-96': show }"
>
<div
class="justify-center items-center flex flex-wrap border-b rounded-b border-secondary h-full"
:class="{ 'pl-12': !show }"
>
<slot name="header" />
</div>
@ -17,26 +30,25 @@
<div class="h-full overflow-scroll bg-base-200">
<slot />
</div>
</div>
</div> -->
</template>
<script setup lang="ts">
const { show } = storeToRefs(useSidebarStore());
const emit = defineEmits(['close', 'submit']);
const emit = defineEmits(["close", "submit"]);
const { escape, enter } = useMagicKeys();
watchEffect(async () => {
if (escape.value) {
await nextTick();
emit('close');
emit("close");
}
});
watchEffect(async () => {
if (enter.value) {
await nextTick();
emit('submit');
emit("submit");
}
});
</script>

View File

@ -1,21 +1,21 @@
import { H3Error } from 'h3';
import { H3Error } from "h3";
export const bytesToBase64DataUrlAsync = async (
bytes: Uint8Array,
type = 'application/octet-stream'
type = "application/octet-stream"
) => {
return await new Promise((resolve, reject) => {
const reader = Object.assign(new FileReader(), {
onload: () => resolve(reader.result),
onerror: () => reject(reader.error),
});
reader.readAsDataURL(new File([new Blob([bytes])], '', { type }));
reader.readAsDataURL(new File([new Blob([bytes])], "", { type }));
});
};
export const blobToImageAsync = (blob: Blob): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
console.log('transform blob', blob);
console.log("transform blob", blob);
const url = URL.createObjectURL(blob);
let img = new Image();
img.onload = () => {
@ -34,7 +34,7 @@ export const deepToRaw = <T extends Record<string, any>>(sourceObj: T): T => {
if (isRef(input) || isReactive(input) || isProxy(input)) {
return objectIterator(toRaw(input));
}
if (input && typeof input === 'object') {
if (input && typeof input === "object") {
return Object.keys(input).reduce((acc, key) => {
acc[key as keyof typeof acc] = objectIterator(input[key]);
return acc;
@ -48,10 +48,9 @@ export const deepToRaw = <T extends Record<string, any>>(sourceObj: T): T => {
export const readableFileSize = (sizeInByte: number | string = 0) => {
if (!sizeInByte) {
return '0 KB';
return "0 KB";
}
const size =
typeof sizeInByte === 'string' ? parseInt(sizeInByte) : sizeInByte;
const size = typeof sizeInByte === "string" ? parseInt(sizeInByte) : sizeInByte;
const sizeInKb = size / 1024;
const sizeInMb = sizeInKb / 1024;
const sizeInGb = sizeInMb / 1024;
@ -64,20 +63,17 @@ export const readableFileSize = (sizeInByte: number | string = 0) => {
return `${sizeInKb.toFixed(2)} KB`;
};
import type { LocationQueryValue, RouteLocationRawI18n } from 'vue-router';
import type { LocationQueryValue, RouteLocationRawI18n } from "vue-router";
export const getSingleRouteParam = (
param: string | string[] | LocationQueryValue | LocationQueryValue[]
): string => {
const _param = Array.isArray(param) ? param.at(0) ?? '' : param ?? '';
const _param = Array.isArray(param) ? param.at(0) ?? "" : param ?? "";
//console.log('found param', _param, param);
return _param;
return decodeURIComponent(_param);
};
export const isRouteActive = (
to: RouteLocationRawI18n,
exact: boolean = false
) =>
export const isRouteActive = (to: RouteLocationRawI18n, exact: boolean = false) =>
computed(() => {
const found = useRouter()
.getRoutes()
@ -86,9 +82,7 @@ export const isRouteActive = (
return exact
? found?.name === useRouter().currentRoute.value.name
: found?.name === useRouter().currentRoute.value.name ||
found?.children.some(
(child) => child.name === useRouter().currentRoute.value.name
);
found?.children.some((child) => child.name === useRouter().currentRoute.value.name);
});
export const isKey = <T extends object>(x: T, k: PropertyKey): k is keyof T => {

View File

@ -1,32 +1,27 @@
<template>
<div class="w-full h-full flex flex-col">
<div class="w-full h-full flex flex-col min-w-min">
<nav
class="navbar bg-base-100 max-sm:rounded-box max-sm:shadow sm:border-b border-base-content/25 sm:z-20 relative px-2"
class="navbar bg-base-100 rounded-b max-sm:shadow border-b border-base-content/25 sm:z-20 relative px-2"
>
<button
type="button"
class="btn btn-text btn-square me-2"
class="btn btn-text btn-square me-2 z-50"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
data-overlay="#sidebar"
@click="toogleSidebar"
ref="sidebarToogleRef"
>
<Icon name="mage:dash-menu" size="28" />
</button>
<!-- <button
type="button"
class="btn btn-text max-sm:btn-square me-2"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
data-overlay="#sidebar"
>
<span class="icon-[tabler--menu-2] size-5"></span>
</button> -->
<div class="flex flex-1 items-center">
<a class="link text-base-content link-neutral text-xl font-semibold no-underline" href="#">
<UiTextGradient>Haex Hub</UiTextGradient>
</a>
<NuxtLinkLocale
class="link text-base-content link-neutral text-xl font-semibold no-underline"
:to="{ name: 'vaultOverview' }"
>
<UiTextGradient class="text-nowrap">Haex Hub</UiTextGradient>
</NuxtLinkLocale>
</div>
<div class="navbar-end flex items-center gap-4 me-4">
<div
@ -158,53 +153,35 @@
</div>
</nav>
<aside
id="sidebar"
class="overlay sm:shadow-none overlay-open:translate-x-0 drawer drawer-start hidden sm:absolute max-w-14 sm:flex sm:translate-x-0 sm:pt-12 z-10"
role="dialog"
tabindex="-1"
>
<div class="drawer-body px-0 pt-4">
<ul class="menu p-0">
<!-- <li
<div class="flex h-full">
<aside
id="sidebar"
class="sm:shadow-none drawer max-w-14 transition-all"
:class="[!isVisible ? 'w-0' : 'w-14']"
role="dialog"
tabindex="-1"
>
<div class="drawer-body px-0">
<ul class="menu p-0">
<UiSidebarLink v-bind="item" v-for="item in menu" :key="item.id" />
<UiSidebarLinkExtension
v-bind="item"
v-for="item in availableExtensions"
:key="item.id"
/>
</ul>
</div>
</aside>
> -->
<UiSidebarLink v-bind="item" v-for="item in menu" :key="item.id" />
<!-- <UiTooltip
:tooltip="item.tooltip || item.name"
direction="right-start"
>
<NuxtLinkLocale
:class="{ ['bg-base-300']: isActive(item.id).value }"
:to="{
name: 'extension',
params: { extensionId: item.id, vaultId: 'test' },
}"
class="flex items-center justify-start hover:text-primary cursor-pointer tooltip-toogle"
>
<Icon
:name="item.icon"
class="size-6"
/>
<i
:class="item.icon"
class="shrink-0 size-8"
/>
</NuxtLinkLocale>
</UiTooltip> -->
<!-- </li> -->
</ul>
</div>
</aside>
<div class="overflow-hidden transition-all relative w-full">
<div class="h-full overflow-scroll sm:pl-14">
<slot />
<div class="overflow-hidden transition-all relative w-full">
<div
class="h-full overflow-scroll transition-all pl-0"
:class="[isVisible ? 'sm:pl-14 ' : ' pl-0']"
>
<slot />
</div>
</div>
</div>
<!-- <main class="sm:pl-14">
<NuxtPage />
</main> -->
@ -212,14 +189,26 @@
</template>
<script setup lang="ts">
const { t } = useI18n();
import { NuxtLinkLocale } from "#components";
const { t } = useI18n();
const { menu, isVisible } = storeToRefs(useSidebarStore());
const sidebarToogleRef = useTemplateRef("sidebarToogleRef");
onClickOutside(sidebarToogleRef, () => {
if (currentScreenSize.value === "xs") {
isVisible.value = false;
}
});
const { notifications } = storeToRefs(useNotificationStore());
const { menu } = storeToRefs(useSidebarStore());
const { isActive } = useExtensionsStore();
const { closeAsync } = useVaultStore();
const { currentScreenSize } = storeToRefs(useUiStore());
const onExtensionSelectAsync = async (id: string) => {};
const { availableExtensions } = storeToRefs(useExtensionsStore());
const toogleSidebar = () => {
isVisible.value = !isVisible.value;
};
const onVaultCloseAsync = async () => {
await closeAsync();

View File

@ -1,11 +1,9 @@
<template>
<div class="text-white">
<NuxtLayout name="app">
<NuxtPage />
</NuxtLayout>
</div>
<div class="text-white h-full">
<NuxtLayout name="app">
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
const { createTable } = useVaultStore();
</script>
<script setup lang="ts"></script>

View File

@ -1,7 +1,16 @@
<template>
{{ iframeSrc }}
<div class="w-full h-full">
<iframe class="w-full h-full" @load="" ref="iFrameRef"> </iframe>
<p>{{ t("loading") }}</p>
<iframe
v-if="iframeSrc"
class="w-full h-full"
@load=""
ref="iFrameRef"
:src="iframeSrc"
sandbox="allow-scripts allow-same-origin"
>
</iframe>
<p v-else>{{ t("loading") }}</p>
</div>
</template>
@ -10,8 +19,21 @@ definePageMeta({
name: "haexExtension",
});
const iframeRef = useTemplateRef("iFrameRef");
const { t } = useI18n();
const iframeRef = useTemplateRef("iFrameRef");
const { extensionEntry: iframeSrc, currentExtension } = storeToRefs(useExtensionsStore());
const extensionStore = useExtensionsStore();
watch(iframeSrc, () => console.log("iframeSrc", iframeSrc.value), { immediate: true });
onMounted(async () => {
const minfest = await extensionStore.readManifestFileAsync(
currentExtension.value!.id,
currentExtension.value!.version
);
console.log("manifest", minfest, extensionStore.extensionEntry);
});
</script>
<i18n lang="yaml">

View File

@ -1,9 +1,92 @@
<template>
<div>add extension</div>
<div class="flex flex-col">
<h1>{{ t("title") }}</h1>
<UiButton @click="loadExtensionManifestAsync">
{{ t("extension.add") }}
</UiButton>
<HaexExtensionManifestConfirm
:manifest="extension.manifest!"
v-model:open="showConfirmation"
@confirm="addExtensionAsync"
/>
</div>
</template>
<script setup lang="ts">
import { join } from "@tauri-apps/api/path";
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
definePageMeta({
name: "haexExtensionAdd",
name: "extensionOverview",
});
const { t } = useI18n();
const extensionStore = useExtensionsStore();
const showConfirmation = ref(false);
const extension = reactive<{
manifest: IHaexHubExtensionManifest | null | undefined;
path: string | null;
}>({
manifest: null,
path: "",
});
const loadExtensionManifestAsync = async () => {
try {
extension.path = await open({ directory: true, recursive: true });
if (!extension.path) return;
const manifestFile = JSON.parse(
await readTextFile(await join(extension.path, "manifest.json"))
);
if (!extensionStore.checkManifest(manifestFile))
throw new Error(`Manifest fehlerhaft ${JSON.stringify(manifestFile)}`);
extension.manifest = manifestFile;
showConfirmation.value = true;
} catch (error) {
console.error("Fehler beim Laden des Moduls:", error);
}
};
const { add } = useSnackbar();
const addExtensionAsync = async () => {
try {
await extensionStore.installAsync(extension.path);
await extensionStore.loadExtensionsAsync();
console.log("Modul erfolgreich geladen");
add({
type: "success",
title: t("extension.success.title", { extension: extension.manifest?.name }),
text: t("extension.success.text"),
});
} catch (error) {
console.error("Fehler beim Laden des Moduls:", error);
add({ type: "error", text: JSON.stringify(error) });
}
};
</script>
<i18n lang="json">
{
"de": {
"title": "Erweiterung installieren",
"extension": {
"add": "Erweiterung hinzufügen",
"success": {
"title": "{extension} hinzugefügt",
"text": "Die Erweiterung wurde erfolgreich hinzugefügt"
}
}
},
"en": {
"title": "Install extension"
}
}
</i18n>

View File

@ -1,9 +1,15 @@
<template>
<div class="h-screen bg-blue-200"></div>
<div class="bg-blue-200 h-full">aaaaa</div>
</template>
<script setup lang="ts">
definePageMeta({
name: "vaultOverview",
name: "vaultOverview",
});
const extensionStore = useExtensionsStore();
onMounted(async () => {
await extensionStore.loadExtensionsAsync();
});
</script>

View File

@ -2,19 +2,29 @@ import { invoke } from "@tauri-apps/api/core";
import { join, resourceDir } from "@tauri-apps/api/path";
import { readTextFile, readDir } from "@tauri-apps/plugin-fs";
import { convertFileSrc } from "@tauri-apps/api/core";
import { haexExtensions } from "~~/src-tauri/database/schemas/vault";
import { eq } from "drizzle-orm";
const manifestFileName = "manifest.json";
const logoFileName = "logo.svg";
export interface IHaexHubExtensionLink {
name: string;
icon: string;
tooltip?: string;
id: string;
version: string;
manifest?: IHaexHubExtensionManifest;
}
export interface IHaexHubExtensionManifest {
name: string;
id: string;
entry: string;
author: string;
url: string;
version: string;
icon: string;
permissions: {
database?: {
read?: string[];
@ -30,80 +40,142 @@ export interface IHaexHubExtensionManifest {
}
export const useExtensionsStore = defineStore("extensionsStore", () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([
{
id: "haex-browser",
name: "Haex Browser",
icon: "solar:global-outline",
},
{
id: "extensions",
name: "sidebar.extensions",
icon: "gg:extension",
},
{
id: "settings",
name: "sidebar.settings",
icon: "ph:gear-six",
},
]);
const availableExtensions = ref<IHaexHubExtensionLink[]>([]);
const currentRoute = useRouter().currentRoute;
const isActive = (id: string) =>
computed(
() =>
currentRoute.value.name === "extension" &&
currentRoute.value.params.extensionId === id
() => currentRoute.value.name === "extension" && currentRoute.value.params.extensionId === id
);
const currentExtension = computed(() => {
if (currentRoute.value.name !== "haexExtension") return;
const extensionId = getSingleRouteParam(
currentRoute.value.params.extensionId
);
console.log("computed currentExtension", currentRoute.value.params);
if (currentRoute.value.meta.name !== "haexExtension") return;
const extensionId = getSingleRouteParam(currentRoute.value.params.extensionId);
console.log("extensionId from param", extensionId);
if (!extensionId) return;
return availableExtensions.value.find(
(extension) => extension.id === extensionId
);
const extension = availableExtensions.value.find((extension) => extension.id === extensionId);
console.log("currentExtension", extension);
return extension;
});
const checkExtensionDirectoryAsync = async (extensionDirectory: string) => {
const getExtensionPathAsync = async (extensionId?: string, version?: string) => {
if (!extensionId || !version) return "";
return await join(await resourceDir(), "extensions", extensionId, version);
};
const checkSourceExtensionDirectoryAsync = async (extensionDirectory: string) => {
try {
const dir = await readDir(extensionDirectory);
const manifest = dir.find(
(entry) => entry.name === manifestFileName && entry.isFile
);
const manifest = dir.find((entry) => entry.name === manifestFileName && entry.isFile);
if (!manifest) throw new Error("Kein Manifest für Erweiterung gefunden");
const logo = dir.find((item) => item.isFile && item.name === logoFileName);
if (!logo) throw new Error("Logo fehlt");
return true;
} catch (error) {
throw new Error(
`Keine Leseberechtigung für Ordner ${extensionDirectory}`
);
throw new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
}
};
const installAsync = async (
extensionDirectory: string | null,
global: boolean = true
): Promise<void> => {
const checkManifest = (manifestFile: unknown): manifestFile is IHaexHubExtensionManifest => {
const errors = [];
if (typeof manifestFile !== "object" || manifestFile === null) {
errors.push("Manifest ist falsch");
return false;
}
if (!("id" in manifestFile) || typeof manifestFile.id !== "string")
errors.push("Keine ID vergeben");
if (!("name" in manifestFile) || typeof manifestFile.name !== "string")
errors.push("Name fehlt");
if (!("entry" in manifestFile) || typeof manifestFile.entry !== "string")
errors.push("Entry fehlerhaft");
if (!("author" in manifestFile) || typeof manifestFile.author !== "string")
errors.push("Author fehlt");
if (!("url" in manifestFile) || typeof manifestFile.url !== "string") errors.push("Url fehlt");
if (!("version" in manifestFile) || typeof manifestFile.version !== "string")
errors.push("Version fehlt");
if (
!("permissions" in manifestFile) ||
typeof manifestFile.permissions !== "object" ||
manifestFile.permissions === null
) {
errors.push("Berechtigungen fehlen");
}
if (errors.length) throw errors;
/* const permissions = manifestFile.permissions as Partial<IHaexHubExtensionManifest["permissions"]>;
if (
("database" in permissions &&
(typeof permissions.database !== "object" || permissions.database === null)) ||
("filesystem" in permissions && typeof permissions.filesystem !== "object") ||
permissions.filesystem === null
) {
return false;
} */
return true;
};
const readManifestFileAsync = async (extensionId: string, version: string | number) => {
try {
if (!extensionDirectory)
throw new Error("Kein Ordner für Erweiterung angegeben");
const checkDirectory = await checkExtensionDirectoryAsync(
extensionDirectory
);
const extensionPath = await getExtensionPathAsync(extensionId, `${version}`);
const manifestPath = await join(extensionPath, manifestFileName);
const manifest = (await JSON.parse(
await readTextFile(manifestPath)
)) as IHaexHubExtensionManifest;
const manifestPath = await join(extensionDirectory, "manifest.json");
const manifest = await JSON.parse(await readTextFile(manifestPath));
/*
TODO implement await checkManifets(manifest);
*/
return manifest;
} catch (error) {
console.error("ERROR readManifestFileAsync", error);
}
};
console.log("manifest", manifest);
return;
const installAsync = async (extensionDirectory: string | null, global: boolean = true) => {
try {
if (!extensionDirectory) throw new Error("Kein Ordner für Erweiterung angegeben");
const manifestPath = await join(extensionDirectory, manifestFileName);
const manifest = (await JSON.parse(
await readTextFile(manifestPath)
)) as IHaexHubExtensionManifest;
const destination = await getExtensionPathAsync(manifest.id, manifest.version);
await checkSourceExtensionDirectoryAsync(extensionDirectory);
await invoke("copy_directory", { source: extensionDirectory, destination });
const logoFilePath = await join(destination, "logo.svg");
const logoSvg = await readTextFile(logoFilePath);
const { currentVault } = storeToRefs(useVaultStore());
const res = await currentVault.value?.drizzle.insert(haexExtensions).values({
id: manifest.id,
name: manifest.name,
author: manifest.author,
enabled: true,
url: manifest.url,
version: manifest.version,
icon: logoSvg,
});
console.log("insert extensions", res);
} catch (error) {
throw error;
/*
@ -213,9 +285,158 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
}
};
const extensionEntry = computedAsync(
async () => {
console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log("extension id or entry missing", currentExtension.value);
return "no mani: " + currentExtension.value;
}
//return "wadahadedzdz";
const extensionPath = await getExtensionPathAsync(
currentExtension.value?.id,
currentExtension.value?.version
); //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
console.log("extensionEntry extensionPath", extensionPath);
const manifest = await readManifestFileAsync(
currentExtension.value.id,
currentExtension.value.version
);
if (!manifest) return "no manifest readable";
const entryPath = await join(extensionPath, manifest.entry);
return `asset://localhost/${entryPath}`;
let entryHtml = await readTextFile(entryPath);
console.log("entryHtml", entryHtml);
const replacements = [];
let match;
while ((match = regex.exec(entryHtml)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith("http")) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const fileContent = await readTextFile(await join(extensionPath, resource));
const blob = new Blob([fileContent], { type: getMimeType(resource) });
const blobUrl = URL.createObjectURL(blob);
console.log("blob", resource, blobUrl);
entryHtml = entryHtml.replace(match, `${prefix}${blobUrl}${suffix}`);
}
console.log("entryHtml", entryHtml);
const blob = new Blob([entryHtml], { type: "text/html" });
const iframeSrc = URL.createObjectURL(blob);
console.log("iframeSrc", iframeSrc);
/* const path = convertFileSrc(extensionDir, manifest.entry);
console.log("final path", path); */
//manifest.entry = iframeSrc;
return iframeSrc;
/* await join(
path, //`file:/${extensionDirectory}`,
manifest.entry
); */
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
manifestPath,
}); */
/* const iframe = document.createElement('iframe');
iframe.src = manifest.entry;
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none'; */
/* const addonApi = {
db_execute: async (sql: string, params: string[] = []) => {
return invoke('db_execute', {
addonId: manifest.name,
sql,
params,
});
},
db_select: async (sql: string, params: string[] = []) => {
return invoke('db_select', {
addonId: manifest.name,
sql,
params,
});
},
}; */
/* iframe.onload = () => {
iframe.contentWindow?.postMessage(
{ type: 'init', payload: addonApi },
'*'
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
},
null,
{ lazy: true }
);
const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore());
/* const query = db
.select()
.from(haexExtensions)
//.where(sql`${haexExtensions.enabled} = "1"`);
.where(eq(haexExtensions.enabled, true)); */
const extensions = await currentVault.value?.drizzle
.select()
.from(haexExtensions)
.where(eq(haexExtensions.enabled, true));
//const manifest = readTextFile(manifestFileName)
//const { sql, params } = query.toSQL();
//const extensions = await invoke<any>("sql_select", { sql, params });
console.log("loadExtensionsAsync ", extensions);
availableExtensions.value =
extensions?.map((extension) => ({
id: extension.id,
name: extension.name ?? "",
icon: extension.icon ?? "",
tooltip: extension.name ?? "",
version: extension.version ?? "",
})) ?? [];
};
return {
availableExtensions,
checkManifest,
currentExtension,
extensionEntry,
installAsync,
isActive,
loadExtensionsAsync,
readManifestFileAsync,
};
});
const getMimeType = (file: string) => {
if (file.endsWith(".css")) return "text/css";
if (file.endsWith(".js")) return "text/javascript";
return "text/plain";
};

View File

@ -2,31 +2,33 @@ import { getSingleRouteParam } from "~/composables/helper";
import type { RouteLocationRaw, RouteLocationAsRelativeGeneric } from "vue-router";
export interface ISidebarItem {
name: string;
icon: string;
tooltip?: string;
id: string;
to?: RouteLocationAsRelativeGeneric;
name: string;
icon: string;
tooltip?: string;
id: string;
to?: RouteLocationAsRelativeGeneric;
}
export const useSidebarStore = defineStore("sidebarStore", () => {
const menu = ref<ISidebarItem[]>([
{
id: "haex-browser",
name: "Haex Browser",
icon: "solar:global-outline",
to: { name: "haexBrowser" },
},
const isVisible = ref(true);
{
id: "haex-extensions-add",
name: "Haex Extensions",
icon: "gg:extension",
to: { name: "haexExtensionAdd" },
},
]);
const menu = ref<ISidebarItem[]>([
{
id: "haex-browser",
name: "Haex Browser",
icon: "solar:global-outline",
to: { name: "haexBrowser" },
},
/* const loadAsync = async (id: string) => {
{
id: "haex-extensions-add",
name: "Haex Extensions",
icon: "gg:extension",
to: { name: "extensionOverview" },
},
]);
/* const loadAsync = async (id: string) => {
extensions.value.some(async (extension) => {
if (extension.id === id) {
await navigateTo(
@ -37,8 +39,9 @@ export const useSidebarStore = defineStore("sidebarStore", () => {
});
}; */
return {
menu,
//loadAsync,
};
return {
menu,
isVisible,
//loadAsync,
};
});

View File

@ -57,85 +57,83 @@ export const useVaultStore = defineStore("vaultStore", () => {
);
const openAsync = async ({ path = "", password }: { path: string; password: string }) => {
const sqlitePath = path?.startsWith("sqlite:") ? path : `sqlite:${path}`;
try {
console.log("try to open db", path, password);
console.log("try to open db", path, password);
const result = await invoke<string>("open_encrypted_database", {
path,
key: password,
});
console.log("open vault from store", result);
if (!(await testDatabaseReadAsync())) throw new Error("Passwort falsch");
//const db = await Database.load(sqlitePath);
const vaultId = await getVaultIdAsync(path);
const seperator = platform() === "windows" ? "\\" : "/";
const fileName = path.split(seperator).pop();
console.log("opened db fileName", fileName, vaultId);
openVaults.value = {
...openVaults.value,
[vaultId]: {
//database: db,
const result = await invoke<string>("open_encrypted_database", {
path,
password,
name: fileName ?? path,
drizzle: drizzle<typeof schema>(
async (sql, params, method) => {
let rows: any = [];
let results = [];
key: password,
});
// If the query is a SELECT, use the select method
if (isSelectQuery(sql)) {
rows = await invoke("db_select", { sql, params }).catch((e) => {
console.error("SQL Error:", e);
return [];
});
} else {
// Otherwise, use the execute method
rows = await invoke("db_execute", { sql, params }).catch((e) => {
console.error("SQL Error:", e);
return [];
});
return { rows: [] };
}
console.log("open vault from store", result);
//const db = await Database.load(sqlitePath);
rows = rows.map((row: any) => {
return Object.values(row);
});
const vaultId = await getVaultIdAsync(path);
const seperator = platform() === "windows" ? "\\" : "/";
const fileName = path.split(seperator).pop();
console.log("opened db fileName", fileName, vaultId);
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0];
openVaults.value = {
...openVaults.value,
[vaultId]: {
//database: db,
path,
password,
name: fileName ?? path,
drizzle: drizzle<typeof schema>(
async (sql, params: unknown[], method) => {
let rows: any = [];
let results = [];
return { rows: results };
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
),
},
};
// If the query is a SELECT, use the select method
if (isSelectQuery(sql)) {
console.log("sql_select", sql, params);
rows = await invoke("sql_select", { sql, params }).catch((e) => {
console.error("SQL select Error:", e, sql, params);
return [];
});
console.log("select", rows);
} else {
// Otherwise, use the execute method
rows = await invoke("sql_execute", { sql, params }).catch((e) => {
console.error("SQL execute Error:", e, sql, params);
return [];
});
return { rows: [] };
}
const { addVaultAsync } = useLastVaultStore();
await addVaultAsync({ path });
/* rows = rows.map((row: any) => {
return Object.values(row);
}); */
return vaultId;
};
console.log("select after map", rows);
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0];
const createTable = () => {
console.log("ddd", schema.testTable.getSQL().queryChunks);
return { rows: results };
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
),
},
};
schema.testTable.getSQL().queryChunks.forEach((chunk) => {
const res = currentVault.value?.drizzle.run(chunk);
console.log("create table", res);
});
//if (!(await testDatabaseReadAsync())) throw new Error("Passwort falsch");
const { addVaultAsync } = useLastVaultStore();
await addVaultAsync({ path });
return vaultId;
} catch (error) {
console.error("Error openAsync ", error);
return false;
//if (error === "file is not a database") throw new Error("Passwort falsch");
}
};
const testDatabaseReadAsync = async () => {
try {
currentVault.value?.drizzle.select({ count: count() }).from(schema.haexExtensions);
return true;
return currentVault.value?.drizzle.select({ count: count() }).from(schema.haexExtensions);
} catch (error) {
return false;
}
@ -186,7 +184,6 @@ export const useVaultStore = defineStore("vaultStore", () => {
openVaults,
refreshDatabaseAsync,
read_only,
createTable,
};
});