zwischenstand

This commit is contained in:
2025-05-28 11:35:02 +02:00
parent 07ff15aba0
commit 4774d3fdc1
105 changed files with 4129 additions and 1438 deletions

View File

@ -1,29 +1,29 @@
<template>
<div class="browser">
<div class="browser-controls">
<button @click="$emit('goBack', activeTabId)" :disabled="!activeTabId">
<button :disabled="!activeTabId" @click="$emit('goBack', activeTabId)">
</button>
<button @click="$emit('goForward', activeTabId)" :disabled="!activeTabId">
<button :disabled="!activeTabId" @click="$emit('goForward', activeTabId)">
</button>
<button @click="$emit('createTab')">+</button>
<HaexBrowserUrlBar
:url="activeTab?.url || ''"
:isLoading="activeTab?.isLoading || false"
:is-loading="activeTab?.isLoading || false"
@submit="handleUrlSubmit"
/>
</div>
<HaexBrowserTabBar
:tabs="tabs"
:activeTabId="activeTabId"
@closeTab="$emit('closeTab', $event)"
@activateTab="$emit('activateTab', $event)"
:active-tab-id="activeTabId"
@close-tab="$emit('closeTab', $event)"
@activate-tab="$emit('activateTab', $event)"
/>
<div class="browser-content" ref="contentRef">
<div ref="contentRef" class="browser-content">
<!-- Die eigentlichen Webview-Inhalte werden von Tauri verwaltet -->
<div v-if="!activeTabId" class="empty-state">
<p>

View File

@ -1,6 +1,6 @@
<template>
<form class="url-bar" @submit.prevent="handleSubmit">
<input type="text" v-model="inputValue" placeholder="URL eingeben" />
<input v-model="inputValue" type="text" placeholder="URL eingeben" >
<span v-if="isLoading" class="loading-indicator">Laden...</span>
<button v-else type="submit">Go</button>
</form>

View File

@ -1,5 +1,6 @@
<template>
<button :class="cn(
<button
:class="cn(
`relative flex items-center justify-center min-w-28 min-h-10 overflow-hidden outline-2 outline-offset-2 rounded cursor-pointer`, className
)
">

View File

@ -1,39 +1,50 @@
<template>
<div class="card">
<slot name="image" />
<div
class="card border-4 shadow-md shadow-accent h-48 w-48 overflow-hidden hover:shadow-xl transition-shadow "
v-bind="$attrs">
<div class="absolute top-2 right-2">
<UiButton class="btn-error btn-outline btn-sm btn-square" @click="$emit('remove')">
<Icon name="mdi:trash" />
</UiButton>
<UiDropdown class="btn btn-sm btn-text btn-circle">
<template #activator>
<Icon name="mdi:dots-vertical" />
</template>
<template #items>
<UiButton class="btn-error btn-outline btn-sm " @click="showRemoveDialog = true">
<Icon name="mdi:trash" /> {{ t("remove") }}
</UiButton>
</template>
</UiDropdown>
</div>
<div class="card-header">
<div v-if="$slots.title || name">
<div class="flex justify-start gap-4">
<div v-html="icon" class="shrink-0 size-10" />
<h5 v-if="name" class="card-title m-0 my-auto">
{{ name }}
</h5>
</div>
</div>
<div class="text-base-content/50">{{ manifest }}</div>
<h5 v-if="name" class="card-title">
{{ name }}
</h5>
</div>
<div class="card-body">
<slot />
<div
class="card-body relative cursor-pointer"
@click="navigateTo(useLocalePath()({ name: 'haexExtension', params: { extensionId: id } }))">
<!-- <slot />
<div class="card-actions" v-if="$slots.action">
<slot name="action" />
</div>
</div> -->
<div class="size-20 absolute bottom-2 right-2" v-html="icon" />
</div>
<!-- <div class="card-footer">
</div> -->
</div>
<HaexExtensionDialogRemove v-model:open="showRemoveDialog" :extension @confirm="removeExtensionAsync" />
</template>
<script setup lang="ts">
import type { IHaexHubExtension } from "~/types/haexhub";
const emit = defineEmits(["close", "submit", "remove"]);
defineProps<IHaexHubExtension>();
const extension = defineProps<IHaexHubExtension>();
const { escape, enter } = useMagicKeys();
@ -50,4 +61,67 @@ watchEffect(async () => {
emit("submit");
}
});
const showRemoveDialog = ref(false)
const { add } = useSnackbar()
const { t } = useI18n()
const extensionStore = useExtensionsStore()
const removeExtensionAsync = async () => {
if (!extension?.id || !extension?.version) {
add({ type: 'error', text: 'Erweiterung kann nicht gelöscht werden' })
return
}
try {
await extensionStore.removeExtensionAsync(
extension.id,
extension.version
)
await extensionStore.loadExtensionsAsync()
add({
type: 'success',
title: t('extension.remove.success.title', {
extensionName: extension.name,
}),
text: t('extension.remove.success.text', {
extensionName: extension.name,
}),
})
} catch (error) {
add({
type: 'error',
title: t('extension.remove.error.title'),
text: t('extension.remove.error.text', { error: JSON.stringify(error) }),
})
}
}
</script>
<i18n lang="yaml">
de:
remove: Löschen
extension:
remove:
success:
text: 'Erweiterung {extensionName} wurde erfolgreich entfernt'
title: '{extensionName} entfernt'
error:
text: "Erweiterung {extensionName} konnte nicht entfernt werden. \n {error}"
title: 'Fehler beim Entfernen von {extensionName}'
en:
remove: Remove
extension:
remove:
success:
text: 'Extension {extensionName} was removed'
title: '{extensionName} removed'
error:
text: "Extension {extensionName} couldn't be removed. \n {error}"
title: 'Exception during uninstall {extensionName}'
</i18n>

View File

@ -0,0 +1,118 @@
<template>
<UiDialogConfirm v-model:open="open" @abort="onDeny" @confirm="onConfirm">
<template #title>
<i18n-t keypath="question" tag="p">
<template #extension>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
</i18n-t>
</template>
<div class="flex flex-col">
<nav class="tabs tabs-bordered" aria-label="Tabs" role="tablist" aria-orientation="horizontal">
<button
v-show="manifest?.permissions?.database" id="tabs-basic-item-1" type="button"
class="tab active-tab:tab-active active" data-tab="#tabs-basic-1" aria-controls="tabs-basic-1" role="tab" aria-selected="true">
{{ t("database") }}
</button>
<button
v-show="manifest?.permissions?.filesystem" id="tabs-basic-item-2" type="button"
class="tab active-tab:tab-active" data-tab="#tabs-basic-2" aria-controls="tabs-basic-2" role="tab" aria-selected="false">
{{ t("filesystem") }}
</button>
<button
v-show="manifest?.permissions?.http" id="tabs-basic-item-3" type="button"
class="tab active-tab:tab-active" data-tab="#tabs-basic-3" aria-controls="tabs-basic-3" role="tab" aria-selected="false">
{{ t("http") }}
</button>
</nav>
<div class="mt-3 min-h-40">
<div id="tabs-basic-1" role="tabpanel" aria-labelledby="tabs-basic-item-1">
<HaexExtensionManifestPermissionsDatabase :database="permissions?.database" />
</div>
<div id="tabs-basic-2" class="hidden" role="tabpanel" aria-labelledby="tabs-basic-item-2">
<HaexExtensionManifestPermissionsFilesystem :filesystem="permissions?.filesystem" />
</div>
<div id="tabs-basic-3" class="hidden" role="tabpanel" aria-labelledby="tabs-basic-item-3">
<HaexExtensionManifestPermissionsHttp :http="permissions?.http" />
</div>
</div>
</div>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import type { IHaexHubExtensionManifest } from "~/types/haexhub";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
const { manifest } = defineProps<{ manifest?: IHaexHubExtensionManifest | null }>();
const permissions = computed(() => ({
database: {
read: manifest?.permissions.database?.read?.map(read => ({
[read]: true
})),
write: manifest?.permissions.database?.read?.map(write => ({
[write]: true
})),
create: manifest?.permissions.database?.read?.map(create => ({
[create]: true
})),
},
filesystem: {
read: manifest?.permissions.filesystem?.read?.map(read => ({
[read]: true
})),
write: manifest?.permissions.filesystem?.write?.map(write => ({
[write]: true
})),
},
http: manifest?.permissions.http?.map(http => ({
[http]: true
})),
}))
watch(permissions, () => console.log("permissions", permissions.value))
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": "Erweiterung {extension} hinzufügen?",
"confirm": "Bestätigen",
"deny": "Ablehnen",
"database": "Datenbank",
"http": "Internet",
"filesystem": "Dateisystem"
},
"en": {
"title": "Confirm Permission",
"question": "Add Extension {extension}?",
"confirm": "Confirm",
"deny": "Deny",
"database": "Database",
"http": "Internet",
"filesystem": "Filesystem"
}
}</i18n>

View File

@ -0,0 +1,33 @@
<template>
<UiDialogConfirm v-model:open="open">
<template #title>
<i18n-t keypath="title" tag="p">
<template #extensionName>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
</i18n-t>
</template>
<p>{{ t("question", { extensionName: manifest?.name }) }}</p>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import type { IHaexHubExtensionManifest } from "~/types/haexhub";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
const { manifest } = defineProps<{ manifest?: IHaexHubExtensionManifest | null }>();
</script>
<i18n lang="yaml">
de:
title: "{extensionName} bereits installiert"
question: Soll die Erweiterung {extensionName} erneut installiert werden?
en:
title: "{extensionName} is already installed"
question: Do you want to reinstall {extensionName}?
</i18n>

View File

@ -1,5 +1,5 @@
<template>
<UiDialog :title="t('title')" v-model:open="open">
<UiDialogConfirm v-model:open="open" :title="t('title')" @confirm="onConfirm">
<div>
<i18n-t keypath="question" tag="p">
<template #name>
@ -7,16 +7,7 @@
</template>
</i18n-t>
</div>
<template #buttons>
<UiButton class="btn-outline btn-error" @click="open = false">
<Icon name="mdi:cancel" /> {{ t("abort") }}
</UiButton>
<UiButton class="btn-error" @click="onConfirm">
<Icon name="mdi:trash" /> {{ t("remove") }}
</UiButton>
</template>
</UiDialog>
</UiDialogConfirm>
</template>
<script setup lang="ts">
@ -36,8 +27,7 @@ const onConfirm = () => {
};
</script>
<i18n lang="json">
{
<i18n lang="json">{
"de": {
"title": "Erweiterung löschen",
"question": "Soll {name} wirklich gelöscht werden?",
@ -46,9 +36,8 @@ const onConfirm = () => {
},
"en": {
"title": "Remove Extension",
"question": "Soll {name} wirklich gelöscht werden?",
"question": "Should {name} really be deleted?",
"abort": "Abort",
"remove": "Remove"
}
}
</i18n>
}</i18n>

View File

@ -1,104 +0,0 @@
<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">
import type { IHaexHubExtensionManifest } from "~/types/haexhub";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
defineProps<{ manifest?: IHaexHubExtensionManifest | null }>();
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

@ -1,63 +1,71 @@
<template>
<div>
<HaexExtensionManifestPermissionsTitle>
{{ t("database.title") }}
</HaexExtensionManifestPermissionsTitle>
<UiAccordion v-if="database?.read?.length">
<template #title>
<h3>{{ t("permission.read") }}</h3>
</template>
<div v-if="database?.read?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.read") }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="read in database?.read" class="flex items-center justify-between px-4 py-1">
<div class="flex items-center gap-1">
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{ Object.keys(read).at(0) }}</label>
</div>
</li>
</ul>
</UiAccordion>
<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>
<UiAccordion v-if="database?.write?.length">
<template #title>
<h3>{{ t("permission.write") }}</h3>
</template>
<div v-if="database?.write?.length">
<UiAccordion>
<template #title>
<h3>{{ t("permission.write") }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="write in database?.write" class="flex items-center justify-between px-4 py-0.5">
<div class="flex items-center gap-1">
<input
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(write).at(0)" >
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{ Object.keys(write).at(0) }}</label>
</div>
</li>
</ul>
</UiAccordion>
<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>
<UiAccordion v-if="database?.create?.length">
<template #title>
<h3>{{ t("permission.create") }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="create in database?.create" class="flex items-center justify-between px-4 py-0.5">
<div class="flex items-center gap-1">
<input
:id="Object.keys(create).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(create).at(0)" >
<label class="label-text text-base" :for="Object.keys(create).at(0)">{{ Object.keys(create).at(0) }}</label>
</div>
</li>
</ul>
</UiAccordion>
</template>
<script setup lang="ts">
defineProps<{ database: IHaexHubExtensionManifest["permissions"]["database"] }>();
defineProps<{ database?: { read?: Record<string, boolean>[], write?: Record<string, boolean>[], create?: Record<string, boolean>[] } }>();
const { t } = useI18n();
</script>
<i18n lang="json">
{
"de": {
"permission": {
"read": "Lesen",
"write": "Schreiben"
},
<i18n lang="yaml">
de:
permission:
read: Lesen
write: Schreiben
create: Erstellen
"database": {
"title": "Datenbank Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
en:
permission:
read: Read
write: Write
create: Create
</i18n>

View File

@ -1,65 +1,58 @@
<template>
<div>
<HaexExtensionManifestPermissionsTitle>
{{ t("filesystem.title") }}
</HaexExtensionManifestPermissionsTitle>
<UiAccordion v-if="filesystem?.read?.length">
<template #title>
<h3>{{ t('permission.read') }}</h3>
</template>
<ul class="space-y-0.5">
<li v-for="read in filesystem?.read" class="flex items-center justify-between px-4 py-0.5">
<div class="flex items-center gap-1">
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{
Object.keys(read).at(0)
}}</label>
</div>
</li>
</ul>
</UiAccordion>
<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>
<UiAccordion v-if="filesystem?.write?.length">
<template #title>
<h3>{{ t('permission.write') }}</h3>
</template>
<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>
<ul class="space-y-0.5">
<li v-for="write in filesystem?.write" class="flex items-center justify-between px-4 py-0.5">
<div class="flex items-center gap-1">
<input
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(write).at(0)" >
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{
Object.keys(write).at(0)
}}</label>
</div>
</li>
</ul>
</UiAccordion>
</template>
<script setup lang="ts">
defineProps<{ filesystem: IHaexHubExtensionManifest["permissions"]["filesystem"] }>();
const { t } = useI18n();
defineProps<{
filesystem?: {
read?: Record<string, boolean>[]
write?: Record<string, boolean>[]
}
}>()
const { t } = useI18n()
</script>
<i18n lang="json">
{
"de": {
"permission": {
"read": "Lesen",
"write": "Schreiben"
},
<i18n lang="yaml">
de:
permission:
read: Lesen
write: Schreiben
"filesystem": {
"title": "Dateisystem Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
en:
permission:
read: Read
write: Write
</i18n>

View File

@ -1,43 +1,33 @@
<template>
<div>
<HaexExtensionManifestPermissionsTitle>
{{ t("http.title") }}
</HaexExtensionManifestPermissionsTitle>
<UiAccordion>
<template #title>
<h3>{{ t("http.access") }}</h3>
</template>
<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>
<ul class="space-y-0.5">
<li v-for="access in http" class="flex items-center justify-between px-4 py-0.5">
<div class="flex items-center gap-1">
<input
:id="Object.keys(access).at(0)" type="checkbox" class="checkbox"
:checked="Object.values(access).at(0)" >
<label class="label-text text-base" :for="Object.keys(access).at(0)">{{ Object.keys(access).at(0) }}</label>
</div>
</li>
</ul>
</UiAccordion>
</template>
<script setup lang="ts">
defineProps<{ http: IHaexHubExtensionManifest["permissions"]["http"] }>();
defineProps<{ http?: Record<string, boolean>[] }>();
const { t } = useI18n();
</script>
<i18n lang="json">
{
"de": {
"permission": {
"access": "Zugriff"
},
<i18n lang="yaml">
de:
http:
access: Internet Zugriff
"http": {
"title": "Internet Berechtigungen"
}
},
"en": { "title": "Confirm Permission" }
}
en:
http:
access: Internet Access
</i18n>

View File

@ -6,7 +6,8 @@
<!-- <div class="">
-->
<HaexButton v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
<HaexButton
v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
@click="copy(`${input}`)">
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</Haexbutton>
@ -19,7 +20,7 @@
<label class="floating-label input join-item">
<Icon v-if="iconPrepend" :name="iconPrepend" class="my-auto size-6" />
<span>Your Email</span>
<input type="text" placeholder="mail@site.com" class=" join-item " />
<input type="text" placeholder="mail@site.com" class=" join-item " >
<Icon v-if="iconAppend" :name="iconAppend" class="my-auto shrink-0" />
</label>
@ -28,13 +29,14 @@
<slot name="append" class="h-auto" />
<HaexButton v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
<HaexButton
v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
@click="copy(`${input}`)">
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</Haexbutton>
</fieldset>
<span class="flex flex-col px-2 pt-0.5" v-show="errors">
<span v-show="errors" class="flex flex-col px-2 pt-0.5">
<span v-for="error in errors" class="label-text-alt text-error">
{{ error }}
</span>

View File

@ -14,7 +14,7 @@
<li>
<NuxtLinkLocale class="dropdown-item" :to="{ name: 'haexSettings' }">
<span class="icon-[tabler--settings]"></span>
<span class="icon-[tabler--settings]"/>
{{ t('settings') }}
</NuxtLinkLocale>
</li>
@ -23,7 +23,7 @@
<li class="dropdown-footer gap-2">
<button class="btn btn-error btn-soft btn-block" @click="onVaultCloseAsync">
<span class="icon-[tabler--logout]"></span>
<span class="icon-[tabler--logout]"/>
{{ t('vault.close') }}
</button>
</li>

View File

@ -0,0 +1,190 @@
<template>
<VaultCardEdit
v-if="vaultEntry.details" v-model:read_only="read_only" :color="currentGroup?.color || 'text-base-content'"
:has-changes="hasChanges" :icon="vaultEntry.details?.icon || icon || 'mdi:key-outline'"
:title="vaultEntry.details?.title ?? ''" @back="$emit('back')" @close="$emit('close')"
@reject="(to) => $emit('reject', to)" @submit="(to) => $emit('submit', to)">
<div class="h-full relative overflow-hidden">
<nav
aria-label="Tabs Vault Entry" aria-orientation="horizontal"
class="tabs tabs-bordered w-full transition-all duration-700 sticky top-0 z-10 bg-base-200" role="tablist">
<button
:id="id.details" aria-controls="vaultDetailsId" aria-selected="true"
class="tab active-tab:tab-active active w-full" data-tab="#vaultDetailsId" role="tab" type="button">
<Icon name="material-symbols:key-outline" class="me-2" />
<span class="hidden sm:block">
{{ t('tab.details') }}
</span>
</button>
<button
:id="id.keyValue" aria-controls="tabs-basic-2" aria-selected="false"
class="tab active-tab:tab-active w-full" data-tab="#tabs-basic-2" role="tab" type="button">
<Icon name="fluent:group-list-20-filled" class="me-2" />
<span class="hidden sm:block">
{{ t('tab.keyValue') }}
</span>
</button>
<button
:id="id.history" aria-controls="tabs-basic-3" aria-selected="false"
class="tab active-tab:tab-active w-full" data-tab="#tabs-basic-3" role="tab" type="button">
<Icon name="material-symbols:history" class="me-2" />
<span class="hidden sm:block">
{{ t('tab.history') }}
</span>
</button>
</nav>
<div class="h-full pb-8">
<div id="vaultDetailsId" role="tabpanel" :aria-labelledby="id.details" class="h-full">
<VaultEntryDetails v-if="vaultEntry.details" v-model="vaultEntry.details" :with-copy-button :read_only/>
</div>
<div id="tabs-basic-2" class="hidden" role="tabpanel" :aria-labelledby="id.keyValue">
{{ originally }}
</div>
<div id="tabs-basic-3" class="hidden h-full" role="tabpanel" :aria-labelledby="id.history">
<VaultEntryHistory v-if="vaultEntry.history" :history="vaultEntry.history" />
</div>
</div>
</div>
</VaultCardEdit>
<!-- <VaultModalSaveChanges
v-model="showConfirmation"
@reject="onReject"
@submit="onSubmit"
/> -->
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
const { t } = useI18n();
const vaultEntry = defineModel<IVaultEntryComplete>({ required: true });
const { currentGroup } = storeToRefs(useVaultGroupStore());
/* watch(
() => vaultEntry.value.details,
() => {
header.value.text = vaultEntry.value.details?.title;
header.value.icon =
vaultEntry.value.details?.icon || currentGroup.value?.icon;
},
{ immediate: true }
); */
const id = reactive({
details: useId(),
keyValue: useId(),
history: useId(),
content: {},
});
const read_only = defineModel<boolean>('read_only', { default: false });
const props = defineProps({
icon: String,
originally: Object as PropType<IVaultEntryComplete>,
title: String,
withCopyButton: Boolean,
});
const emit = defineEmits<{
submit: [to?: RouteLocationNormalizedLoadedGeneric];
close: [void];
back: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
const showConfirmation = ref(false);
const hasChanges = computed(() => {
if (!props.originally?.details) {
if (
vaultEntry.value.details?.note?.length ||
vaultEntry.value.details?.password?.length ||
vaultEntry.value.details?.tags?.length ||
vaultEntry.value.details?.title?.length ||
vaultEntry.value.details?.url?.length ||
vaultEntry.value.details?.urlAliases?.length ||
vaultEntry.value.details?.username?.length
) {
console.log('has changes', props.originally, vaultEntry.value);
return true;
} else {
return false;
}
}
return (
JSON.stringify(props.originally.details) !==
JSON.stringify(vaultEntry.value.details)
);
});
const to = ref<RouteLocationNormalizedLoadedGeneric>();
const isSaved = ref(false);
const isRejected = ref(false);
const onSubmit = () => {
console.log('entry onSubmit');
showConfirmation.value = false;
isSaved.value = true;
emit('submit', to.value);
};
const onReject = () => {
console.log('entry onReject');
showConfirmation.value = false;
isRejected.value = true;
emit('reject', to.value);
};
const onBack = () => {
console.log('entry onBack', read_only.value);
if (hasChanges.value) {
showConfirmation.value = true;
} else {
emit('back');
}
};
/* onBeforeRouteLeave((_to, _from, next) => {
console.log('check before leave', _to, _from);
to.value = _to;
if (isSaved.value || isRejected.value) {
isSaved.value = false;
isRejected.value = false;
next();
} else if (hasChanges.value) {
showConfirmation.value = true;
} else {
next();
}
}); */
</script>
<i18n lang="json">{
"de": {
"create": "Anlegen",
"abort": "Abbrechen",
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "Verlauf"
}
},
"en": {
"create": "Create",
"abort": "Abort",
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "History"
}
}
}</i18n>

View File

@ -0,0 +1,165 @@
<template>
<aside :id ref="sidebarRef" class=" flex sm:shadow-none w-full md:max-w-64 bg-red-200" tabindex="-1">
<div class="drawer-body w-full ">
<ul class="menu space-y-0.5 p-0 rounded-none md:rounded">
<li>
<a href="#">
<span class="icon-[tabler--home] size-5"/>
Home
</a>
</li>
<li class="space-y-0.5">
<a id="menu-app" class="collapse-toggle collapse-open:bg-base-content/10" data-collapse="#menu-app-collapse">
<span class="icon-[tabler--apps] size-5"/>
Apps
<span
class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4 transition-all duration-300"/>
</a>
<ul
id="menu-app-collapse"
class="collapse hidden w-auto space-y-0.5 overflow-hidden transition-[height] duration-300"
aria-labelledby="menu-app">
<li>
<a href="#">
<span class="icon-[tabler--message] size-5"/>
Chat
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--calendar] size-5"/>
Calendar
</a>
</li>
<li class="space-y-0.5">
<a
id="sub-menu-academy" class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#sub-menu-academy-collapse">
<span class="icon-[tabler--book] size-5"/>
Academy
<span class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4"/>
</a>
<ul
id="sub-menu-academy-collapse"
class="collapse hidden w-auto space-y-0.5 overflow-hidden transition-[height] duration-300"
aria-labelledby="sub-menu-academy">
<li>
<a href="#">
<span class="icon-[tabler--books] size-5"/>
Courses
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--list-details] size-5"/>
Course details
</a>
</li>
<li class="space-y-0.5">
<a
id="sub-menu-academy-stats" class="collapse-toggle collapse-open:bg-base-content/10"
data-collapse="#sub-menu-academy-stats-collapse">
<span class="icon-[tabler--chart-bar] size-5"/>
Stats
<span class="icon-[tabler--chevron-down] collapse-open:rotate-180 size-4"/>
</a>
<ul
id="sub-menu-academy-stats-collapse"
class="collapse hidden w-auto space-y-0.5 overflow-hidden transition-[height] duration-300"
aria-labelledby="sub-menu-academy-stats">
<li>
<a href="#">
<span class="icon-[tabler--chart-donut] size-5"/>
Goals
</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<a href="#">
<span class="icon-[tabler--settings] size-5"/>
Settings
</a>
</li>
<div class="divider text-base-content/50 py-6 after:border-0">Account</div>
<li>
<a href="#">
<span class="icon-[tabler--login] size-5"/>
Sign In
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--logout-2] size-5"/>
Sign Out
</a>
</li>
<div class="divider text-base-content/50 py-6 after:border-0">Miscellaneous</div>
<li>
<a href="#">
<span class="icon-[tabler--users-group] size-5"/>
Support
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--files] size-5"/>
Documentation
</a>
</li>
</ul>
</div>
</aside>
</template>
<script setup lang="ts">
import type { HSOverlay } from "flyonui/flyonui";
defineProps<{ title?: string; label?: string }>();
defineEmits(["open", "close"]);
const id = useId();
const open = defineModel<boolean>("open", { default: true });
const { t } = useI18n();
const sidebarRef = useTemplateRef("sidebarRef");
const modal = ref<HSOverlay>();
watch(open, async () => {
if (open.value) {
await modal.value?.open();
} else {
await modal.value?.close(true);
}
});
onMounted(async () => {
if (!sidebarRef.value) return;
modal.value = new window.HSOverlay(sidebarRef.value, {
isClosePrev: true,
});
modal.value.on("close", () => {
open.value = false;
});
});
</script>
<i18n lang="yaml">
de:
close: Schließen
en:
close: Close
</i18n>

View File

@ -1,16 +1,10 @@
<template>
<li
@click="triggerNavigate"
class="hover:text-primary rounded"
:class="{ ['bg-base-200 text-base-content']: isActive }"
>
class="hover:text-primary rounded" :class="{ ['bg-base-200 text-base-content']: isActive }"
@click="triggerNavigate">
<UiTooltip :tooltip="tooltip ?? name" direction="right-start">
<NuxtLinkLocale
:to
class="flex items-center justify-center cursor-pointer tooltip-toogle"
ref="linkRef"
>
<div v-if="iconType === 'svg'" v-html="icon" class="shrink-0 size-5" />
<NuxtLinkLocale ref="linkRef" :to class="flex items-center justify-center cursor-pointer tooltip-toogle">
<div v-if="iconType === 'svg'" class="shrink-0 size-5" v-html="icon" />
<Icon v-else :name="icon" size="1.5em" />
</NuxtLinkLocale>
</UiTooltip>
@ -18,7 +12,7 @@
</template>
<script setup lang="ts">
import { type ISidebarItem } from '#imports'
import type { ISidebarItem } from '#imports'
const props = defineProps<ISidebarItem>()

View File

@ -0,0 +1,37 @@
<template>
<div class="accordion divide-neutral/20 divide-y accordion-shadow *:accordion-item-active:shadow-md">
<div :id="itemId" ref="accordionRef" class="accordion-item active">
<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"/>
<slot name="title" />
</button>
<div
:id="collapseId" class="accordion-content w-full overflow-hidden transition-[height] duration-300"
:aria-labelledby="itemId" role="region">
<slot />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { 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 window.HSAccordion(accordionRef.value);
accordion.value.hide()
}
});
</script>

View File

@ -0,0 +1,52 @@
<template>
<UiDialog v-model:open="open" @close="onAbort">
<template #trigger>
<slot name="trigger" />
</template>
<template #title>
<slot name="title" />
</template>
<slot />
<template #buttons>
<slot name="buttons">
<UiButton class="btn-error btn-outline" @click="onAbort">
<Icon name="mdi:close" /> {{ abortLabel ?? t("abort") }}
</UiButton>
<UiButton class="btn-primary " @click="onConfirm">
<Icon name="mdi:check" /> {{ confirmLabel ?? t("confirm") }}
</UiButton>
</slot>
</template>
</UiDialog>
</template>
<script setup lang="ts">
defineProps<{ confirmLabel?: string, abortLabel?: string }>()
const open = defineModel<boolean>("open", { default: false })
const { t } = useI18n()
const emit = defineEmits(["confirm", "abort"])
const onAbort = () => {
open.value = false
emit("abort")
}
const onConfirm = () => {
open.value = false
emit("confirm")
}
</script>
<i18n lang="yaml">
de:
abort: Abbrechen
confirm: Bestätigen
en:
abort: Abort
confirm: Confirm
</i18n>

View File

@ -1,29 +1,58 @@
<template>
<button v-bind="$attrs" type="button" aria-haspopup="dialog" aria-expanded="false" :aria-label="label"
class="--prevent-on-load-init " @click="$emit('open')">
<slot name="trigger">open</slot>
<button
v-if="$slots.trigger || label"
v-bind="$attrs"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
:aria-label="label"
class="--prevent-on-load-init"
@click="$emit('open')"
>
<slot name="trigger">
{{ label }}
</slot>
</button>
<div :id class="overlay modal overlay-open:opacity-100 hidden overlay-open:duration-300" role="dialog" ref="modalRef"
tabindex="-1">
<div
:id
ref="modalRef"
class="overlay modal overlay-open:opacity-100 hidden overlay-open:duration-300 modal-middle"
role="dialog"
tabindex="-1"
>
<div
class="overlay-animation-target overlay-open:mt-4 overlay-open:duration-500 mt-12 transition-all ease-out modal-dialog overlay-open:opacity-100">
<div class="modal-content">
class="overlay-animation-target overlay-open:mt-4 overlay-open:duration-500 mt-12 transition-all ease-out modal-dialog overlay-open:opacity-100"
>
<div class="modal-content gap-2">
<div class="modal-header">
<slot name="title">
<h3 v-if="title" class="modal-title text-base sm:text-lg">
<div
v-if="title || $slots.title"
class="modal-title"
>
<slot name="title">
{{ title }}
</h3>
</slot>
</slot>
</div>
<button type="button" class="btn btn-text btn-circle btn-sm absolute end-3 top-3" :aria-label="t('close')"
tabindex="1" @click="open = false">
<Icon name="mdi:close" size="18" />
<button
type="button"
class="btn btn-text btn-circle btn-sm absolute end-3 top-3"
:aria-label="t('close')"
tabindex="1"
@click="open = false"
>
<Icon
name="mdi:close"
size="18"
/>
</button>
</div>
<div class="modal-body text-sm sm:text-base py-1">
<div class="modal-body text-sm sm:text-base">
<slot />
</div>
<div class="modal-footer flex-wrap">
<slot name="buttons" />
</div>
@ -33,65 +62,50 @@
</template>
<script setup lang="ts">
import type { HSOverlay } from "flyonui/flyonui";
import type { HSOverlay } from 'flyonui/flyonui'
defineOptions({
inheritAttrs: false,
});
defineProps<{ title?: string; label?: string }>()
defineProps<{ title?: string; label?: string }>();
const emit = defineEmits(['open', 'close'])
defineEmits(["open", "close"]);
const id = useId()
const id = useId();
const open = defineModel<boolean>('open', { default: false })
const open = defineModel<boolean>("open", { default: false });
const { t } = useI18n()
const { t } = useI18n();
const modalRef = useTemplateRef('modalRef')
const modalRef = useTemplateRef("modalRef");
defineExpose({ modalRef })
defineExpose({ modalRef });
const modal = ref<HSOverlay>();
const modal = ref<HSOverlay>()
watch(open, async () => {
console.log("watch open modal", open.value, modal.value);
if (open.value) {
await modal.value?.open();
await modal.value?.open()
} else {
await modal.value?.close(true);
//HSOverlay.close(`#${id}`);
//console.log("close dialog");
await modal.value?.close(true)
emit('close')
}
});
})
onMounted(async () => {
if (!modalRef.value) return;
// flyonui has a problem importing HSOverlay at component level due to ssr
// that's the workaround I found
//const flyonui = await import("flyonui/flyonui");
if (!modalRef.value) return
modal.value = new window.HSOverlay(modalRef.value, {
isClosePrev: true,
});
})
modal.value.on("close", () => {
console.log("on close", open.value);
open.value = false;
});
/* modal.value.on("open", () => {
console.log("on open", open.value);
open.value = true;
}); */
});
modal.value.on('close', () => {
open.value = false
})
})
</script>
<i18n lang="json">{
"de": {
"close": "Schließen"
},
"en": {
"close": "Close"
}
}</i18n>
<i18n lang="yaml">
de:
close: Schließen
en:
close: Close
</i18n>

View File

@ -1,20 +1,20 @@
<template>
<div class="dropdown relative inline-flex">
<button :id class="dropdown-toggle " v-bind="$attrs" aria-haspopup="menu" aria-expanded="false" :aria-label="label">
<button :id class="dropdown-toggle" v-bind="$attrs" aria-haspopup="menu" aria-expanded="false" :aria-label="label">
<slot name="activator">
{{ label }}
<span class="icon-[tabler--chevron-down] dropdown-open:rotate-180 size-4">
</span>
<span class="icon-[tabler--chevron-down] dropdown-open:rotate-180 size-4"/>
</slot>
</button>
<ul class="dropdown-menu dropdown-open:opacity-100 hidden min-w-28" role="menu" aria-orientation="vertical"
<ul
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-28" role="menu" aria-orientation="vertical"
:aria-labelledby="id">
<slot name="items">
<li v-for="item in items" :is="itemIs" class="dropdown-item" @click="$emit('select', item)">
<li :is="itemIs" v-for="item in items" class="dropdown-item" @click="$emit('select', item)">
<slot name="item" :item>
{{ item }}
</slot>

View File

@ -1,6 +1,7 @@
<template>
<UiDropdown :items="availableLocales" @select="(locale) => $emit('select', locale)"
class="btn btn-primary btn-outline">
<UiDropdown
:items="availableLocales" class="btn btn-primary btn-outline"
@select="(locale) => $emit('select', locale)">
<template #activator>
<Icon :name="flags[locale]" />
@ -20,7 +21,7 @@
</template>
<script setup lang="ts">
import { type Locale } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
const flags = {
de: 'emojione:flag-for-germany',

View File

@ -1,5 +1,5 @@
<template>
<UiDropdown :items="availableThemes" @select="(theme) => $emit('select', theme)" class="btn btn-primary btn-outline">
<UiDropdown :items="availableThemes" class="btn btn-primary btn-outline" @select="(theme) => $emit('select', theme)">
<template #activator>
<Icon :name="currentTheme.icon" />
</template>

View File

@ -1,14 +1,15 @@
<template>
<div>
<fieldset class="join w-full pt-0.5">
<fieldset class="join w-full pt-1.5 " v-bind="$attrs">
<slot name="prepend" />
<div class="input join-item">
<Icon v-if="prependIcon" :name="prependIcon" class="my-auto shrink-0" />
<div class="input-floating grow">
<input :id :name="name ?? id" :placeholder="placeholder || label" :type :autofocus class="ps-3"
v-bind="$attrs" v-model="input" ref="inputRef" :readonly="read_only" />
<input
:id ref="inputRef" v-model="input" :name="name ?? id" :placeholder="placeholder || label" :type
:autofocus class="ps-3" :readonly="read_only" >
<label class="input-floating-label" :for="id">{{ label }}</label>
</div>
@ -17,13 +18,14 @@
<slot name="append" class="h-auto" />
<UiButton v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
<UiButton
v-if="withCopyButton" class="btn-outline btn-accent btn-square join-item h-auto"
@click="copy(`${input}`)">
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
</fieldset>
<span class="flex flex-col px-2 pt-0.5" v-show="errors">
<span v-show="errors" class="flex flex-col px-2 pt-0.5">
<span v-for="error in errors" class="label-text-alt text-error">
{{ error }}
</span>
@ -32,7 +34,7 @@
</template>
<script setup lang="ts">
import { type ZodSchema } from "zod";
import type { ZodSchema } from "zod";
const inputRef = useTemplateRef("inputRef");
defineExpose({ inputRef });

View File

@ -1,6 +1,7 @@
<template>
<UiInput :check-input :label="label || t('password')" :placeholder="placeholder || t('password')" :rules :type="type"
:autofocus v-model="value">
<UiInput
v-model="value" :check-input :label="label || t('password')" :placeholder="placeholder || t('password')" :rules
:type="type" :autofocus>
<template #append>
<UiButton class="btn-outline btn-accent btn-square h-auto" @click="tooglePasswordType">
<Icon :name="type === 'password' ? 'mdi:eye-off' : 'mdi:eye'" />

View File

@ -1,51 +1,59 @@
<template>
<svg id="logo" class="fill-current stroke-current w-[160px]" version="1.1" xmlns="http://www.w3.org/2000/svg"
<svg
id="logo" class="fill-current stroke-current w-[160px]" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 286.3 85" xml:space="preserve">
<switch>
<g>
<g class="logo-imagesss">
<circle fill="white" cx="42.5" cy="42.5" r="40"></circle>
<path d="M42.3,83.4c-22.6,0-40.9-18.4-40.9-40.9c0-22.6,18.4-40.9,40.9-40.9c22.6,0,40.9,18.4,40.9,40.9
<circle fill="white" cx="42.5" cy="42.5" r="40"/>
<path
d="M42.3,83.4c-22.6,0-40.9-18.4-40.9-40.9c0-22.6,18.4-40.9,40.9-40.9c22.6,0,40.9,18.4,40.9,40.9
C83.3,65.1,64.9,83.4,42.3,83.4z M42.3,5.8C22.1,5.8,5.7,22.3,5.7,42.5s16.5,36.7,36.7,36.7S79,62.7,79,42.5S62.6,5.8,42.3,5.8z
"></path>
"/>
<g>
<g>
<polygon points="38.8,69.8 38.8,31.7 22.3,31.7 22.3,38.5 29.8,38.5 29.8,69.8 "></polygon>
<path d="M34.1,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6
C39.9,15.9,37.3,13.2,34.1,13.2z"></path>
<polygon points="38.8,69.8 38.8,31.7 22.3,31.7 22.3,38.5 29.8,38.5 29.8,69.8 "/>
<path
d="M34.1,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6
C39.9,15.9,37.3,13.2,34.1,13.2z"/>
</g>
<g>
<polygon points="45.9,69.8 45.9,31.7 62.4,31.7 62.4,38.5 54.9,38.5 54.9,69.8 "></polygon>
<path d="M50.6,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C44.8,15.9,47.4,13.2,50.6,13.2z"></path>
<polygon points="45.9,69.8 45.9,31.7 62.4,31.7 62.4,38.5 54.9,38.5 54.9,69.8 "/>
<path
d="M50.6,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C44.8,15.9,47.4,13.2,50.6,13.2z"/>
</g>
</g>
</g>
<g class="logo-textsss">
<path
d="M136.1,63.6c-4,0-5.3-2.6-5.3-6V38.5h10.6v-6.7h-10.6v-6.7h-9c0,7,0,29.1,0,32.7
c0,4.2,1.6,7.5,3.8,9.7c2.3,2.2,5.6,3.3,9.8,3.3c5.1,0,8.4-1.8,10.6-4.2l-4.7-6C140.2,62.1,138.5,63.6,136.1,63.6z">
</path>
<path d="M217.7,30.7c-4.9,0-8.2,1.6-10.4,3.8c-2.2-2.2-5.5-3.8-10.4-3.8c-15,0-14.9,12.1-14.9,15
c0,4.2,1.6,7.5,3.8,9.7c2.3,2.2,5.6,3.3,9.8,3.3c5.1,0,8.4-1.8,10.6-4.2l-4.7-6C140.2,62.1,138.5,63.6,136.1,63.6z"/>
<path
d="M217.7,30.7c-4.9,0-8.2,1.6-10.4,3.8c-2.2-2.2-5.5-3.8-10.4-3.8c-15,0-14.9,12.1-14.9,15
s0,24.1,0,24.1h9V45.7c0-8.5,4.9-8.3,5.9-8.3c1,0,5.9-0.3,5.9,8.3v24.1h0h9h0V45.7c0-8.5,4.9-8.3,5.9-8.3c1,0,5.9-0.3,5.9,8.3
v24.1h9c0,0,0-21.2,0-24.1C232.6,42.8,232.7,30.7,217.7,30.7z"></path>
<path d="M273.2,46.4c-4.3-1.4-6-2.5-6-5.2c0-2,1.1-3.8,4.3-3.8c3.2,0,4.5,3.3,5.1,4.8
v24.1h9c0,0,0-21.2,0-24.1C232.6,42.8,232.7,30.7,217.7,30.7z"/>
<path
d="M273.2,46.4c-4.3-1.4-6-2.5-6-5.2c0-2,1.1-3.8,4.3-3.8c3.2,0,4.5,3.3,5.1,4.8
c2.7-1.5,5.3-2.9,6.6-3.6c-2.5-6-6.3-7.9-12-7.9c-8,0-11.7,5.5-11.7,10.6c0,6.5,2.9,9.8,11.2,12.2c6,1.8,6.5,4.7,6.2,6.2
c-0.3,1.7-1.6,3.6-5.3,3.6c-3.6,0-5.8-3.8-6.8-5.4c-1.8,1.1-3.4,2.1-6.4,3.8c2.1,5,6.8,9.1,13.5,9.1c7.9,0,12.9-5.1,12.9-12.1
C284.9,51,279.6,48.5,273.2,46.4z"></path>
C284.9,51,279.6,48.5,273.2,46.4z"/>
<g>
<polygon points="239.7,69.8 239.7,31.7 256.2,31.7 256.2,38.5 248.7,38.5 248.7,69.8 "></polygon>
<path d="M244.4,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C238.6,15.9,241.2,13.2,244.4,13.2z"></path>
<polygon points="239.7,69.8 239.7,31.7 256.2,31.7 256.2,38.5 248.7,38.5 248.7,69.8 "/>
<path
d="M244.4,13.2c3.3,0,6,2.6,6,5.9c0,3.3-2.6,6-5.9,6c-3.3,0-6-2.6-6-6
C238.6,15.9,241.2,13.2,244.4,13.2z"/>
</g>
<g>
<polygon points="114.7,69.8 114.7,31.7 98.1,31.7 98.1,38.5 105.7,38.5 105.7,69.8 "></polygon>
<path d="M110,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6C115.8,15.9,113.2,13.2,110,13.2
z"></path>
<polygon points="114.7,69.8 114.7,31.7 98.1,31.7 98.1,38.5 105.7,38.5 105.7,69.8 "/>
<path
d="M110,13.2c-3.3,0-6,2.6-6,5.9c0,3.3,2.6,6,5.9,6c3.3,0,6-2.6,6-6C115.8,15.9,113.2,13.2,110,13.2
z"/>
</g>
<path d="M176.4,52.4v-3.7c0-12.3-4.7-18-14.8-18c-9.3,0-14.7,6.6-14.7,18v4c0,11.5,5.8,18.2,15.8,18.2
<path
d="M176.4,52.4v-3.7c0-12.3-4.7-18-14.8-18c-9.3,0-14.7,6.6-14.7,18v4c0,11.5,5.8,18.2,15.8,18.2
c6.6,0,10.8-3.7,12.7-7.9c-2.2-1.4-4.6-2.8-6.1-3.8c-1,1.7-2.9,4.4-6.7,4.4c-5.8,0-7-5.9-7-10.9v-0.2H176.4z M155.7,45.7
c0.2-7.1,3.3-8.2,6-8.2c2.6,0,5.9,1,6,8.2H155.7z"></path>
c0.2-7.1,3.3-8.2,6-8.2c2.6,0,5.9,1,6,8.2H155.7z"/>
</g>
</g>
</switch>

View File

@ -1,14 +1,23 @@
<template>
<div class="tooltip [--prevent-popper:false]">
<div class="tooltip-toggle" aria-label="Tooltip">
<div
class="tooltip-toggle"
aria-label="Tooltip"
>
<slot>
<button class="btn btn-square">
<Icon name="mdi:chevron-up-box-outline" />
</button>
</slot>
<span class="tooltip-content tooltip-shown:opacity-100 tooltip-shown:visible z-40" role="tooltip">
<span class="tooltip-body" v-bind="$attrs">
<span
class="tooltip-content tooltip-shown:opacity-100 tooltip-shown:visible z-40"
role="tooltip"
>
<span
class="tooltip-body"
v-bind="$attrs"
>
{{ tooltip }}
</span>
</span>
@ -17,7 +26,7 @@
</template>
<script setup lang="ts">
import type { PropType } from 'vue';
import type { PropType } from 'vue'
const props = defineProps({
direction: {
@ -38,15 +47,18 @@ const props = defineProps({
default: 'top',
},
tooltip: String,
tooltip: {
type: String,
default: '',
},
trigger: {
type: String as PropType<'focus' | 'hover' | 'click'>,
default: 'hover',
},
});
})
defineOptions({
inheritAttrs: false,
});
</script>
})
</script>

View File

@ -0,0 +1,26 @@
<template>
<div
class="tree-view-selected:bg-base-200/60 dragged:bg-primary/20 dragged:rounded nested-4 cursor-pointer rounded-md px-2"
role="treeitem" :data-tree-view-item="JSON.stringify({
value,
isDir: false,
})
">
<div class="flex items-center gap-x-3">
<span class="icon-[tabler--file] text-base-content size-4 flex-shrink-0"/>
<div class="grow">
<span class="text-base-content">{{ value }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
value: String,
});
const id = useId();
const controlId = useId();
const isActive = ref(false);
</script>

View File

@ -0,0 +1,204 @@
<template>
<div :id ref="folderRef" data-nested-draggable="" :value class="">
<div
isDir :data-tree-view-item="JSON.stringify({ value })"
class="accordion-item active motion-preset-slide-left motion-ease-spring-bouncier" :class="{
'selected': isActive?.value,
'text-base-content': !color,
}" role="treeitem" :style="{ color: color || '' }">
<div
class="accordion-heading tree-view-selected:bg-primary/80 flex items-center gap-x-0.5 rounded-md hover:bg-primary/20 group">
<button class="accordion-toggle btn btn-sm btn-circle btn-text shrink-0" :aria-controls="controlId">
<Icon name="tabler:plus" class="accordion-item-active:rotate-45 size-4 transition-all duration-300" />
</button>
<button class="cursor-pointer rounded-md px-1.5 w-full" @click.stop="$emit('click', value)">
<div class="flex items-center gap-x-3">
<Icon v-if="icon" :name="icon || 'mdi:folder-outline'" class="shrink-0" />
<div class="flex whitespace-nowrap">
{{ value }}
</div>
</div>
</button>
<button
class="sticky right-2 btn btn-sm btn-circle btn-text shrink-0 group-hover:flex hidden ml-auto"
@click.stop="$emit('edit', value)">
<Icon name="mdi:pencil-outline" class="size-4 transition-all duration-300" />
</button>
</div>
<div
:id="controlId" class="accordion-content w-full transition-[height] duration-300" role="group"
:aria-labelledby="id">
<div ref="childRef" class="tree-view-space min-h-1" data-nested-draggable="">
<slot>
<template
v-for="(item, index) in children?.sort(
(a, b) => a.order ?? 0 - (b.order ?? 0)
)" :key="item.id!" :data-tree-view-item="JSON.stringify({ value: item.value })">
<UiTreeFolder
v-if="item.type === 'folder'" :icon="item.icon || 'tabler:folder'"
v-bind="item" @click="(value) => $emit('click', value)" @edit="(value) => $emit('edit', value)" />
<UiTreeFile v-if="item.type === 'file'" v-bind="item" />
</template>
</slot>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { HSAccordion } from 'flyonui/flyonui';
import Sortable from 'sortablejs';
const props = defineProps({
value: String,
icon: {
type: [String, null],
default: 'tabler:folder',
},
children: {
type: Array as PropType<ITreeItem[] | null>,
default: () => [],
},
name: String,
color: [String, null],
isActive: Object as PropType<ComputedRef<boolean>>,
});
const id = useId();
const controlId = useId();
const folderRef = ref<HTMLElement>();
const childRef = ref<HTMLElement>();
defineEmits<{
click: [value: string | undefined];
edit: [value: string | undefined];
}>();
const { groups } = storeToRefs(useVaultGroupStore());
const sorty = ref([]);
onMounted(() => {
if (folderRef.value && childRef.value)
[folderRef.value, childRef.value].forEach((element) => {
const create = Sortable.create(element, {
animation: 150,
ghostClass: 'bg-opacity-20',
group: 'vault',
swapThreshold: 0.65,
fallbackOnBody: true,
fallbackTolerance: 3,
onEnd: (evt) => {
const { item } = evt;
/* if (item.classList.contains('accordion')) {
let existingInstance = HSAccordion.getInstance(item, true);
let updatedInstance;
existingInstance.element.update();
updatedInstance = HSAccordion.getInstance(item, true);
window.$hsAccordionCollection.map((el) => {
if (
el.element.el !== existingInstance.element.el &&
el.element.group === existingInstance.element.group &&
el.element.el.closest('.accordion') &&
el.element.el.classList.contains('active') &&
existingInstance.element.el.classList.contains('active')
)
el.element.hide();
return el;
});
}
if (!!item.hasAttribute('data-tree-view-item')) {
const treeViewItem = HSTreeView.getInstance(
item.closest('[data-tree-view]'),
true
);
treeViewItem.element.update();
} */
},
onUpdate: (evt) => {
console.log('update', evt.item, props.value, sorty.value);
},
});
/* const sortable = new Sortable(element, {
animation: 150,
ghostClass: 'bg-opacity-20',
group: 'vault',
swapThreshold: 0.65,
fallbackOnBody: true,
fallbackTolerance: 3,
onEnd: (evt) => {
console.log(
'end',
evt.item,
props.value,
sorty.value.at(0).toArray(),
sorty.value.at(1).toArray()
);
},
onUpdate: (evt) => {
console.log('update', evt.item, props.value, sorty.value);
},
});
sorty.value.push(sortable); */
});
});
onMounted(() => {
const draggable = document.querySelectorAll('[data-nested-draggable]');
draggable.forEach((el) => {
const options = {
group: 'nested',
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
ghostClass: 'dragged',
onEnd: (evt) => {
const { item, items } = evt;
console.log('standard', item, evt);
if (item.classList.contains('accordion')) {
const existingInstance = HSAccordion.getInstance(item, true);
let updatedInstance;
existingInstance.element.update();
updatedInstance = HSAccordion.getInstance(item, true);
window.$hsAccordionCollection.map((el) => {
if (
el.element.el !== existingInstance.element.el &&
el.element.group === existingInstance.element.group &&
el.element.el.closest('.accordion') &&
el.element.el.classList.contains('active') &&
existingInstance.element.el.classList.contains('active')
)
el.element.hide();
return el;
});
}
if (item.hasAttribute('data-tree-view-item')) {
const treeViewItem = HSTreeView.getInstance(
item.closest('[data-tree-view]'),
true
);
treeViewItem.element.update();
}
},
};
const data = el.getAttribute('data-nested-draggable');
const dataOptions = data ? JSON.parse(data) : {};
const sortable = new Sortable(el, options);
console.log('stand', sortable.toArray());
});
});
</script>

View File

@ -0,0 +1,5 @@
<template>
<div data-tree-view role="tree" aria-orientation="vertical" class="rounded min-w-fit w-full">
<slot/>
</div>
</template>

19
src/components/ui/tree/types.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
interface ITreeItem {
id: string | null
value: string
name: string | null
icon?: string | null
children?: ITreeItem[] | null
type: 'folder' | 'file'
color?: string | null
order?: number | null
parentId?: string | null
}
interface IVaultGroupTreeItem {
id?: string | null
name: string
icon?: string | null
children?: IVaultGroupTreeItem[] | null
desciption?: string | null
}

View File

@ -1,28 +1,48 @@
<template>
<UiDialog :title="t('title')" v-model:open="open"
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap">
<UiDialog
v-model:open="open"
:title="t('title')"
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap"
>
<template #trigger>
<Icon name="mdi:plus" />
{{ t('database.create') }}
</template>
<form class="flex flex-col gap-4" @submit="onCreateAsync">
<UiInput :check-input="check" :label="t('database.label')" :placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name" autofocus prepend-icon="mdi:safe" v-model="database.name" />
<form
class="flex flex-col gap-4"
@submit="onCreateAsync"
>
<UiInput
v-model="database.name"
:check-input="check"
:label="t('database.label')"
:placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name"
autofocus
prepend-icon="mdi:safe"
/>
<UiInputPassword :check-input="check" :rules="vaultDatabaseSchema.password" prepend-icon="mdi:key-outline"
v-model="database.password" />
<UiInputPassword
v-model="database.password"
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
/>
</form>
<template #buttons>
<UiButton class="btn-error" @click="onClose">
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton class="btn-primary" @click="onCreateAsync">
<UiButton
class="btn-primary"
@click="onCreateAsync"
>
{{ t('create') }}
</UiButton>
</template>
@ -74,7 +94,7 @@ const onCreateAsync = async () => {
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name)
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
database.password,
)
console.log(
@ -82,7 +102,7 @@ const onCreateAsync = async () => {
database.name,
nameCheck,
database.password,
passwordCheck
passwordCheck,
)
if (!nameCheck.success || !passwordCheck.success) return
@ -105,7 +125,7 @@ const onCreateAsync = async () => {
console.log('vaultId', vaultId)
if (vaultId) {
await navigateTo(
useLocaleRoute()({ name: 'vaultOverview', params: { vaultId } })
useLocaleRoute()({ name: 'vaultOverview', params: { vaultId } }),
)
}
}
@ -121,7 +141,8 @@ const onClose = () => {
}
</script>
<i18n lang="json">{
<i18n lang="json">
{
"de": {
"database": {
"label": "Vaultname",
@ -146,4 +167,5 @@ const onClose = () => {
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}</i18n>
}
</i18n>

View File

@ -1,40 +1,52 @@
<template>
<UiDialog v-model:open="isOpen" class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 "
@open="onLoadDatabase">
<UiDialogConfirm
v-model:open="open"
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1"
:confirm-label="t('open')"
:abort-label="t('abort')"
@open="onLoadDatabase"
@abort="onAbort"
>
<template #title>
<i18n-t
keypath="title"
tag="p"
class="flex gap-2"
>
<template #haexvault>
<UiTextGradient>HaexVault</UiTextGradient>
</template>
</i18n-t>
<p class="text-sm">{{ path }}</p>
</template>
<template #trigger>
<Icon name="mdi:folder-open-outline" />
{{ t('database.open') }}
</template>
<UiInputPassword :check-input="check" :rules="vaultDatabaseSchema.password" @keyup.enter="onOpenDatabase" autofocus
prepend-icon="mdi:key-outline" v-model="database.password" />
<template #buttons>
<UiButton class="btn-error" @click="onClose">
{{ t('abort') }}
</UiButton>
<UiButton type="submit" class="btn-primary" @click="onOpenDatabase">
{{ t('open') }}
</UiButton>
</template>
</UiDialog>
<UiInputPassword
v-model="database.password"
:check-input="check"
:rules="vaultDatabaseSchema.password"
autofocus
prepend-icon="mdi:key-outline"
@keyup.enter="onOpenDatabase"
/>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import { open } from '@tauri-apps/plugin-dialog'
import { open as openVault } from '@tauri-apps/plugin-dialog'
import { vaultDatabaseSchema } from './schema'
const { t } = useI18n()
const isOpen = defineModel('isOpen', { type: Boolean })
const open = defineModel('open', { type: Boolean })
const props = defineProps({
path: String,
})
const props = defineProps<{
path: string
}>()
const check = ref(false)
@ -62,7 +74,7 @@ initDatabase()
const { add } = useSnackbar()
const handleError = (error: unknown) => {
isOpen.value = false
open.value = false
console.error('handleError', error, typeof error)
add({ type: 'error', text: 'Passwort falsch' })
}
@ -71,7 +83,7 @@ const { openAsync } = useVaultStore()
const onLoadDatabase = async () => {
try {
database.path = await open({
database.path = await openVault({
multiple: false,
directory: false,
filters: [
@ -82,10 +94,9 @@ const onLoadDatabase = async () => {
],
})
console.log("database.path", database.path)
if (!database.path) return
isOpen.value = true
open.value = true
} catch (error) {
handleError(error)
}
@ -100,7 +111,7 @@ const onOpenDatabase = async () => {
const path = database.path || props.path
const pathCheck = vaultDatabaseSchema.path.safeParse(path)
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
database.password,
)
if (!pathCheck.success || !passwordCheck.success || !path) {
@ -124,7 +135,7 @@ const onOpenDatabase = async () => {
return
}
onClose()
onAbort()
await navigateTo(
localePath({
@ -132,7 +143,7 @@ const onOpenDatabase = async () => {
params: {
vaultId,
},
})
}),
)
await Promise.allSettled([
syncLocaleAsync(),
@ -144,25 +155,24 @@ const onOpenDatabase = async () => {
}
}
const onClose = () => {
const onAbort = () => {
initDatabase()
isOpen.value = false
open.value = false
}
</script>
<i18n lang="json">{
"de": {
"open": "Öffnen",
"abort": "Abbrechen",
"database": {
"open": "Vault öffnen"
}
},
"en": {
"open": "Open",
"abort": "Abort",
"database": {
"open": "Open Vault"
}
}
}</i18n>
<i18n lang="yaml">
de:
open: Öffnen
abort: Abbrechen
title: '{haexvault} entsperren'
database:
open: Vault öffnen
en:
open: Open
abort: Abort
title: Unlock {haexvault}
database:
open: Open Vault
</i18n>

View File

@ -66,7 +66,6 @@
<div
class="flex flex-col items-center w-full min-h-14 gap-2 py-1"
:class="{ '-ml-6': !show }"
:style="{ color }"
>
<Icon
@ -91,10 +90,7 @@
v-show="!read_only"
class="fixed bottom-2 left-0 w-full flex items-center justify-between px-4 md:hidden"
>
<div
class="transition-all duration-500"
:class="{ 'pl-96': show }"
>
<div class="transition-all duration-500">
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
@ -112,7 +108,7 @@
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
<div></div>
<div />
</div>
<!-- <UiButtonAction
class=""
@ -129,89 +125,86 @@
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
const { t } = useI18n();
const { t } = useI18n()
const { show } = storeToRefs(useSidebarStore());
const read_only = defineModel<boolean>('read_only', { default: false })
const read_only = defineModel<boolean>('read_only', { default: false });
const props = defineProps({
color: String,
hasChanges: Boolean,
icon: String,
title: String,
});
const props = defineProps<{
color: string
hasChanges: boolean
icon: string
title: string
}>()
const emit = defineEmits<{
back: [void];
close: [void];
reject: [to?: RouteLocationNormalizedLoadedGeneric];
submit: [to?: RouteLocationNormalizedLoadedGeneric];
}>();
back: [void]
close: [void]
reject: [to?: RouteLocationNormalizedLoadedGeneric]
submit: [to?: RouteLocationNormalizedLoadedGeneric]
}>()
const showConfirmation = ref(false);
const showConfirmation = ref(false)
const to = ref<RouteLocationNormalizedLoadedGeneric>();
const to = ref<RouteLocationNormalizedLoadedGeneric>()
const isApprovedForLeave = ref(false);
const isApprovedForLeave = ref(false)
const wantToGoBack = ref(false);
const wantToGoBack = ref(false)
const onSubmit = () => {
showConfirmation.value = false;
isApprovedForLeave.value = true;
showConfirmation.value = false
isApprovedForLeave.value = true
if (wantToGoBack.value) {
wantToGoBack.value = false;
read_only.value = true;
emit('submit');
wantToGoBack.value = false
read_only.value = true
emit('submit')
} else {
emit('submit', to.value);
emit('submit', to.value)
}
};
}
const onReject = () => {
showConfirmation.value = false;
isApprovedForLeave.value = true;
read_only.value = true;
showConfirmation.value = false
isApprovedForLeave.value = true
read_only.value = true
if (wantToGoBack.value) {
wantToGoBack.value = false;
emit('back');
} else emit('reject', to.value);
};
wantToGoBack.value = false
emit('back')
} else emit('reject', to.value)
}
const onBack = () => {
if (props.hasChanges) {
wantToGoBack.value = true;
showConfirmation.value = true;
wantToGoBack.value = true
showConfirmation.value = true
} else {
emit('back');
emit('back')
}
};
}
const onClose = () => {
if (props.hasChanges) {
showConfirmation.value = true;
showConfirmation.value = true
} else {
emit('close'); //read_only.value = true;
emit('close') //read_only.value = true;
}
};
}
const onDelete = () => {};
onBeforeRouteLeave((_to, _from, next) => {
//console.log('check before leave', _to, _from);
to.value = _to;
to.value = _to
if (isApprovedForLeave.value) {
isApprovedForLeave.value = false;
next();
isApprovedForLeave.value = false
next()
} else if (props.hasChanges) {
showConfirmation.value = true;
showConfirmation.value = true
} else {
next();
next()
}
});
})
</script>
<i18n lang="json">

View File

@ -4,7 +4,7 @@
<div class="card-header">
<div v-if="$slots.title || title">
<Icon :name="icon" />
<Icon v-if="icon" :name="icon" />
<h5 v-if="title" class="card-title mb-0">
{{ title }}
</h5>
@ -16,7 +16,7 @@
<div class="card-body">
<slot />
aaaaaaaaa
<div class="card-actions" v-if="$slots.action">
<div v-if="$slots.action" class="card-actions">
<slot name="action" />
</div>
</div>

View File

@ -1,6 +1,7 @@
<template>
<VaultCardEdit
v-if="vaultGroup"
v-model:read_only="read_only"
:color="vaultGroup.color ?? 'text-base-content'"
:has-changes="hasChanges"
:icon="vaultGroup.icon ?? 'mdi:folder-outline'"
@ -9,42 +10,41 @@
@close="$emit('close')"
@reject="(to) => $emit('reject', to)"
@submit="(to) => $emit('submit', to)"
v-model:read_only="read_only"
>
<div class="flex flex-col gap-4 w-full p-4">
<UiInput
v-show="!read_only"
v-model.trim="vaultGroup.name"
:label="t('vaultGroup.name')"
:placeholder="t('vaultGroup.name')"
:rules="vaultGroupSchema.name"
:with-copy-button="read_only"
:read_only
autofocus
v-model.trim="vaultGroup.name"
/>
<UiInput
v-show="!read_only || vaultGroup.description?.length"
v-model.trim="vaultGroup.description"
:read_only
:label="t('vaultGroup.description')"
:placeholder="t('vaultGroup.description')"
:rules="vaultGroupSchema.description"
:with-copy-button="read_only"
v-model.trim="vaultGroup.description"
/>
<UiColorPicker
v-model="vaultGroup.color"
:read_only
:label="t('vaultGroup.color')"
:placeholder="t('vaultGroup.color')"
v-model="vaultGroup.color"
/>
<UiIconPicker
v-model="vaultGroup.icon"
:read_only
:label="t('vaultGroup.icon')"
:placeholder="t('vaultGroup.icon')"
v-model="vaultGroup.icon"
/>
</div>
</VaultCardEdit>

View File

@ -1,42 +0,0 @@
<template>
<UiListButton
v-if="entry"
:key="entry.id"
@click="navigateToEntryAsync(entry.id)"
class="text-base-content"
>
<div class="flex items-center gap-3">
<div class="w-8">
<Icon
v-if="entry.icon || groupIcon"
:name="entry.icon || groupIcon!"
/>
</div>
<div class="flex flex-col items-start">
<div v-if="!entry.title && !entry.username && !entry.url">
{{ entry.id }}
</div>
<div class="font-semibold">
{{ entry.title }}
</div>
<span class="text-sm">
{{ entry.username }}
</span>
<span class="text-sm">
{{ entry.url }}
</span>
</div>
</div>
</UiListButton>
</template>
<script setup lang="ts">
import type { SelectVaultEntry } from '~/database/schemas/vault';
defineProps({
entry: Object as PropType<SelectVaultEntry>,
groupIcon: [String, null],
});
const { navigateToEntryAsync } = useVaultEntryStore();
</script>

View File

@ -16,9 +16,9 @@
</span>
</UiButton>
<UiButton
ref="abortButtonRef"
class="btn-outline focus:bg-primary"
tabindex="11"
ref="abortButtonRef"
@click="showConfirmation = false"
>
<Icon name="mdi:close" />
@ -41,17 +41,16 @@
</template>
<script setup lang="ts">
const showConfirmation = defineModel<boolean>();
const abortButtonRef = useTemplateRef('abortButtonRef');
const showConfirmation = defineModel<boolean>()
const abortButtonRef = useTemplateRef('abortButtonRef')
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
const { t } = useI18n()
onUpdated(() => {
abortButtonRef.value?.$el.focus();
});
abortButtonRef.value?.$el.focus()
})
defineEmits(['submit', 'reject']);
defineEmits(['submit', 'reject'])
</script>
<i18n lang="json">

View File

@ -1,50 +1,92 @@
<template>
<div class="w-full h-full flex flex-col min-w-min ">
<nav class="navbar bg-base-100 rounded-b max-sm:shadow border-b border-base-content/25 sm:z-20 relative px-2">
<div class="w-full h-full flex flex-col min-w-min">
<nav
class="navbar bg-base-100 rounded-b max-sm:shadow border-b border-base-content/25 sm:z-20 relative px-2"
>
<UiTooltip :tooltip="isVisible ? t('sidebar.close') : t('sidebar.show')">
<button type="button" class="btn btn-text btn-square me-2 z-50" aria-haspopup="dialog" aria-expanded="false"
aria-controls="sidebar" @click="toogleSidebar" ref="sidebarToogleRef">
<Icon :name="isVisible
? 'tabler:layout-sidebar-filled'
: 'tabler:layout-sidebar'
" size="28" />
<button
ref="sidebarToogleRef"
type="button"
class="btn btn-text btn-square me-2 z-50"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
@click="toogleSidebar"
>
<Icon
:name="
isVisible
? 'tabler:layout-sidebar-filled'
: 'tabler:layout-sidebar'
"
size="28"
/>
</button>
</UiTooltip>
<div class="flex flex-1 items-center">
<NuxtLinkLocale class="link text-base-content link-neutral text-xl font-semibold no-underline"
:to="{ name: 'vaultOverview' }">
<UiTextGradient class="text-nowrap">{{
currentVaultName
}}</UiTextGradient>
<NuxtLinkLocale
class="link text-base-content link-neutral text-xl font-semibold no-underline"
:to="{ name: 'vaultOverview' }"
>
<UiTextGradient class="text-nowrap">
{{ currentVaultName }}
</UiTextGradient>
</NuxtLinkLocale>
</div>
<div class="navbar-end flex items-center gap-4 me-4">
<div class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]">
<button id="dropdown-scrollable" type="button"
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle btn btn-text btn-circle dropdown-open:bg-base-content/10 size-10"
aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="indicator">
<span v-show="notifications.length" class="indicator-item bg-error size-2 rounded-full text-sm"></span>
<span class="icon-[tabler--bell] text-base-content size-[1.375rem]"></span>
<span
v-show="notifications.length"
class="indicator-item bg-error size-2 rounded-full text-sm"
/>
<span
class="icon-[tabler--bell] text-base-content size-[1.375rem]"
/>
</div>
</button>
<div class="dropdown-menu dropdown-open:opacity-100 hidden" role="menu" aria-orientation="vertical"
aria-labelledby="dropdown-scrollable">
<div
class="dropdown-menu dropdown-open:opacity-100 hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-scrollable"
>
<div class="dropdown-header justify-center">
<h6 class="text-base-content text-base">
{{ t('notifications.label') }}
</h6>
</div>
<div
class="vertical-scrollbar horizontal-scrollbar rounded-scrollbar text-base-content/80 max-h-56 overflow-auto max-md:max-w-60">
<div class="dropdown-item" v-for="notification in notifications">
class="vertical-scrollbar horizontal-scrollbar rounded-scrollbar text-base-content/80 max-h-56 overflow-auto max-md:max-w-60"
>
<div
v-for="notification in notifications"
:key="notification.date.toDateString()"
class="dropdown-item"
>
<div class="avatar">
<div class="w-10 rounded-full">
<img v-if="notification.image" :src="notification.image"
:alt="notification.alt ?? 'notification avatar'" />
<Icon v-else-if="notification.icon" :name="notification.icon" />
<img
v-if="notification.image"
:src="notification.image"
:alt="notification.alt ?? 'notification avatar'"
/>
<Icon
v-else-if="notification.icon"
:name="notification.icon"
/>
</div>
</div>
<div class="w-60">
@ -57,8 +99,11 @@
</div>
</div>
</div>
<a href="#" class="dropdown-footer justify-center gap-1">
<span class="icon-[tabler--eye] size-4"></span>
<a
href="#"
class="dropdown-footer justify-center gap-1"
>
<span class="icon-[tabler--eye] size-4" />
{{ t('notifications.view_all') }}
</a>
</div>
@ -69,12 +114,26 @@
</nav>
<div class="flex h-full overflow-hidden">
<aside id="sidebar" class="sm:shadow-none transition-all h-full overflow-hidden border-r border-base-300"
:class="[!isVisible ? 'w-0' : 'w-16']" role="dialog" tabindex="-1">
<aside
id="sidebar"
class="sm:shadow-none transition-all h-full overflow-hidden border-r border-base-300"
:class="[!isVisible ? 'w-0' : 'w-16']"
role="dialog"
tabindex="-1"
>
<div class="drawer-body h-full">
<ul class="menu p-0 h-full rounded-none">
<HaexSidebarLink v-bind="item" v-for="item in menu" :key="item.id" />
<HaexSidebarLink v-for="item in extensionLinks" :key="item.id" v-bind="item" icon-type="svg" />
<HaexSidebarLink
v-for="item in menu"
v-bind="item"
:key="item.id"
/>
<HaexSidebarLink
v-for="item in extensionLinks"
:key="item.id"
v-bind="item"
icon-type="svg"
/>
</ul>
</div>
</aside>

View File

@ -4,9 +4,15 @@
<UiDropdownLocale @select="setLocale" />
</div>
<div class="flex flex-col justify-center items-center gap-5 max-w-3xl">
<img src="/logo.svg" class="bg-primary p-3 size-16 rounded-full" alt="HaexVault Logo" />
<img
src="/logo.svg"
class="bg-primary p-3 size-16 rounded-full"
alt="HaexVault Logo"
>
<span class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center">
<span
class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center"
>
<p class="whitespace-nowrap">
{{ t('welcome') }}
</p>
@ -16,21 +22,32 @@
<div class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto">
<VaultButtonCreate />
<VaultButtonOpen v-model:isOpen="passwordPromptOpen" :path="vaultPath" />
<VaultButtonOpen
v-model:open="passwordPromptOpen"
:path="vaultPath"
/>
</div>
<div v-show="lastVaults.length" class="w-full">
<div
v-show="lastVaults.length"
class="w-full"
>
<div class="font-thin text-sm justify-start px-2 pb-1">
{{ t('lastUsed') }}
</div>
<div
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border first:*:rounded-t-md last:*:rounded-b-md overflow-scroll">
<div class="flex items-center justify-between group h-12 overflow-x-scroll" v-for="vault in lastVaults"
:key="vault.path">
class="relative border-base-content/25 divide-base-content/25 flex w-full flex-col divide-y rounded-md border first:*:rounded-t-md last:*:rounded-b-md overflow-scroll"
>
<div
v-for="vault in lastVaults"
:key="vault.path"
class="flex items-center justify-between group h-12 overflow-x-scroll"
>
<button
class="link link-accent flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full py-2 px-4"
@click="; (passwordPromptOpen = true), (vaultPath = vault.path)">
@click=";(passwordPromptOpen = true), (vaultPath = vault.path)"
>
<span class="block md:hidden">
{{ vault.name }}
</span>
@ -38,8 +55,13 @@
{{ vault.path }}
</span>
</button>
<button class="absolute right-2 btn btn-square btn-error btn-xs hidden group-hover:flex min-w-6">
<Icon name="mdi:trash-can-outline" @click="removeVaultAsync(vault.path)" />
<button
class="absolute right-2 btn btn-square btn-error btn-xs hidden group-hover:flex min-w-6"
>
<Icon
name="mdi:trash-can-outline"
@click="removeVaultAsync(vault.path)"
/>
</button>
</div>
</div>
@ -75,7 +97,8 @@ const { lastVaults } = storeToRefs(useLastVaultStore())
await syncLastVaultsAsync()
</script>
<i18n lang="json">{
<i18n lang="json">
{
"de": {
"welcome": "Viel Spass mit",
"lastUsed": "Zuletzt verwendete Vaults",
@ -86,4 +109,5 @@ await syncLastVaultsAsync()
"lastUsed": "Last used Vaults",
"sponsors": "Powered by"
}
}</i18n>
}
</i18n>

View File

@ -1,127 +0,0 @@
<template>
<div class="bg-red-400 h-full">
browser {{ useRouter().currentRoute.value.meta.name }}
<HaexBrowser
:tabs="tabs"
:activeTabId="activeTabId"
@createTab="createNewTab"
@closeTab="closeTab"
@navigate="navigateToUrl"
@goBack="goBack"
@goForward="goForward"
/>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { Window, getCurrentWindow } from "@tauri-apps/api/window";
import { Webview } from "@tauri-apps/api/webview";
definePageMeta({
name: "haexBrowser",
});
interface Tab {
id: string;
title: string;
url: string;
isLoading: boolean;
isActive: boolean;
window_label: string;
}
const tabs = ref<Tab[]>([]);
const activeTabId = ref<string | null>(null);
let unlistenTabCreated: UnlistenFn | null = null;
let unlistenTabClosed: UnlistenFn | null = null;
onMounted(async () => {
// Erstelle einen ersten Tab beim Start
//createNewTab("https://www.google.com");
// Höre auf Tab-Events
unlistenTabCreated = await listen("tab-created", (event) => {
const newTab = event.payload as Tab;
tabs.value = tabs.value.map((tab) => ({
...tab,
isActive: tab.id === newTab.id,
}));
if (!tabs.value.some((tab) => tab.id === newTab.id)) {
tabs.value.push(newTab);
}
activeTabId.value = newTab.id;
});
unlistenTabClosed = await listen("tab-closed", (event) => {
const closedTabId = event.payload as string;
tabs.value = tabs.value.filter((tab) => tab.id !== closedTabId);
});
});
onUnmounted(() => {
if (unlistenTabCreated) unlistenTabCreated();
if (unlistenTabClosed) unlistenTabClosed();
});
const createNewTab = async (url: string = "about:blank") => {
try {
/* const appWindow = new Window('uniqueLabel111', {
fullscreen: true,
});
*/
/* const appWindow = getCurrentWindow();
const webview = new Webview(appWindow, 'theUniqueLabel', {
url: 'https://github.com/tauri-apps/tauri',
height: 1000,
width: 1000,
x: 110,
y: 0,
});
await webview.show(); */
//console.log('create webview', webview);
const tab_id = "foo";
await invoke("create_tab", { url, tabId: "foo" });
} catch (error) {
console.error("Fehler beim Erstellen des Tabs:", error);
}
};
const closeTab = async (tabId: string) => {
try {
//await invoke('close_tab', { tabId });
} catch (error) {
console.error("Fehler beim Schließen des Tabs:", error);
}
};
const navigateToUrl = async (tabId: string, url: string) => {
try {
//await invoke('navigate_to_url', { tabId, url });
} catch (error) {
console.error("Fehler bei der Navigation:", error);
}
};
const goBack = async (tabId: string | null) => {
try {
//await invoke('go_back', { tabId });
} catch (error) {
console.error("Fehler beim Zurückgehen:", error);
}
};
const goForward = async (tabId: string | null) => {
try {
//await invoke('go_forward', { tabId });
} catch (error) {
console.error("Fehler beim Vorwärtsgehen:", error);
}
};
</script>

View File

@ -1,73 +1,28 @@
<template>
<div class="w-full h-full overflow-scroll">
<!-- <div>
{{ iframeSrc }}
</div> -->
<div>
{{ iframeIndex }}
</div>
<iframe
v-if="iframeIndex"
class="w-full h-full"
@load=""
ref="iFrameRef"
class="w-full h-full"
:src="iframeIndex"
sandbox="allow-scripts allow-same-origin"
allow="autoplay; speaker-selection; encrypted-media;"
>
</iframe>
<UiButton @click="go = true">Go</UiButton>
<!-- <p v-else>{{ t("loading") }}</p> -->
{{ audioTest }}
<audio v-if="go" controls :src="audioTest">
Dein Browser unterstützt das Audio-Element nicht.
</audio>
<video v-if="go" controls width="600" :src="demoVideo"></video>
<div v-if="audioError">
Fehler beim Laden der Audio-Datei: {{ audioError }}
</div>
/>
</div>
</template>
<script setup lang="ts">
import { convertFileSrc } from '@tauri-apps/api/core'
import { appDataDir, join, resourceDir } from '@tauri-apps/api/path'
definePageMeta({
name: 'haexExtension',
})
const { t } = useI18n()
const iframeRef = useTemplateRef('iFrameRef')
const { extensionEntry: iframeSrc, currentExtension } = storeToRefs(
useExtensionsStore()
)
const audioAssetUrl = ref('')
const audioError = ref('')
const audioTest = convertFileSrc(
await join(await appDataDir(), 'resources/demo.mp3')
)
const { extensionEntry: iframeSrc } = storeToRefs(useExtensionsStore())
//computed(() => `${iframeSrc.value}/sounds/music/demo.mp3`)
const go = ref(false)
const iframeIndex = computed(() => `${iframeSrc.value}/index.html`)
const demoVideo = computed(() => `${iframeSrc.value}/sounds/music/demo.mp3`)
const extensionStore = useExtensionsStore()
watch(
demoVideo,
async () => {
const res = await fetch(
'/home/haex/.local/share/space.haex.hub/extensions/pokedemo/1.0/sounds/music/demo.mp3'
)
console.log('respo', res)
console.log('iframeSrc', iframeSrc.value)
},
{
immediate: true,
}
const iframeIndex = computed(() =>
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
)
</script>

View File

@ -1,24 +1,31 @@
<template>
<div class="flex flex-col p-1 relative h-full">
<div class="flex" v-if="extensionStore.availableExtensions.length">
<div class="flex flex-col p-4 relative h-full">
<div
v-if="extensionStore.availableExtensions.length"
class="flex"
>
<UiButton
class="fixed top-20 right-4 btn-square btn-primary"
@click="loadExtensionManifestAsync"
@click="prepareInstallExtensionAsyn"
>
<Icon name="mdi:plus" size="1.5em" />
<Icon
name="mdi:plus"
size="1.5em"
/>
</UiButton>
<HaexExtensionCard
v-for="extension in extensionStore.availableExtensions"
v-bind="extension"
@remove="onShowRemoveDialog(extension)"
>
</HaexExtensionCard>
v-for="_extension in extensionStore.availableExtensions"
v-bind="_extension"
:key="_extension.id"
@remove="onShowRemoveDialog(_extension)"
/>
</div>
<!-- <SvgoExtensionsOverview class="h-screen w-screen" /> -->
<!-- <nuxt-icon name="extensions-overview" class="size-full" /> -->
<div v-else class="h-full w-full">
<div
v-else
class="h-full w-full"
>
<Icon
name="my-icon:extensions-overview"
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
@ -27,17 +34,27 @@
<UiTooltip :tooltip="t('extension.add')">
<UiButton
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
@click="loadExtensionManifestAsync"
@click="prepareInstallExtensionAsyn"
>
<Icon name="mdi:plus" size="1.5em" class="rotate-45" />
<Icon
name="mdi:plus"
size="1.5em"
class="rotate-45"
/>
</UiButton>
</UiTooltip>
</div>
</div>
<HaexExtensionManifestConfirm
<HaexExtensionDialogReinstall
v-model:open="openOverwriteDialog"
:manifest="extension.manifest"
@confirm="addExtensionAsync"
/>
<HaexExtensionDialogInstall
v-model:open="showConfirmation"
:manifest="extension.manifest"
@confirm="addExtensionAsync"
/>
@ -45,8 +62,7 @@
v-model:open="showRemoveDialog"
:extension="extensionToBeRemoved"
@confirm="removeExtensionAsync"
>
</HaexExtensionDialogRemove>
/>
</div>
</template>
@ -67,6 +83,7 @@ const { t } = useI18n()
const extensionStore = useExtensionsStore()
const showConfirmation = ref(false)
const openOverwriteDialog = ref(false)
const extension = reactive<{
manifest: IHaexHubExtensionManifest | null | undefined
@ -82,14 +99,13 @@ const loadExtensionManifestAsync = async () => {
if (!extension.path) return
const manifestFile = JSON.parse(
await readTextFile(await join(extension.path, 'manifest.json'))
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
return manifestFile
} catch (error) {
console.error('Fehler loadExtensionManifestAsync:', error)
add({ type: 'error', text: JSON.stringify(error) })
@ -98,6 +114,27 @@ const loadExtensionManifestAsync = async () => {
const { add } = useSnackbar()
const prepareInstallExtensionAsyn = async () => {
try {
const manifest = await loadExtensionManifestAsync()
if (!manifest) throw new Error('No valid Manifest found')
extension.manifest = manifest
const isAlreadyInstalled = await extensionStore.isExtensionInstalledAsync({
id: manifest.id,
version: manifest.version,
})
if (isAlreadyInstalled) {
openOverwriteDialog.value = true
} else {
await addExtensionAsync()
}
} catch (error) {
add({ type: 'error', text: JSON.stringify(error) })
}
}
const addExtensionAsync = async () => {
try {
await extensionStore.installAsync(extension.path)
@ -133,7 +170,7 @@ const removeExtensionAsync = async () => {
try {
await extensionStore.removeExtensionAsync(
extensionToBeRemoved.value.id,
extensionToBeRemoved.value.version
extensionToBeRemoved.value.version,
)
await extensionStore.loadExtensionsAsync()
add({
@ -173,4 +210,17 @@ de:
text: 'Die Erweiterung wurde erfolgreich hinzugefügt'
en:
title: 'Install extension'
extension:
remove:
success:
text: 'Extension {extensionName} was removed'
title: '{extensionName} removed'
error:
text: "Extension {extensionName} couldn't be removed. \n {error}"
title: 'Exception during uninstall {extensionName}'
add: 'Add Extension'
success:
title: '{extension} added'
text: 'Extensions was added successfully'
</i18n>

View File

@ -0,0 +1,8 @@
<template>
<div class="flex">
<HaexPassSidebar />
<div>
<NuxtPage />
</div>
</div>
</template>

View File

@ -0,0 +1,9 @@
<template>
<div>passwords</div>
</template>
<script setup lang="ts">
definePageMeta({
name: "haexpassOverview"
})
</script>

View File

@ -1,5 +1,11 @@
<template>
<div class="h-full text-base-content"></div>
<div class="h-full text-base-content flex bg-base-200 p-4">
<HaexExtensionCard
v-for="extension in extensionStore.availableExtensions"
v-bind="extension"
:key="extension.id"
/>
</div>
</template>
<script setup lang="ts">

View File

@ -10,10 +10,16 @@
<div class="p-2">{{ t('vaultName.label') }}</div>
<div>
<UiInput v-model="currentVaultName" :placeholder="t('vaultName.label')">
<UiInput
v-model="currentVaultName"
:placeholder="t('vaultName.label')"
>
<template #append>
<UiTooltip :tooltip="t('save')">
<UiButton class="btn-primary" @click="onSetVaultNameAsync">
<UiButton
class="btn-primary"
@click="onSetVaultNameAsync"
>
<Icon name="mdi:content-save-outline" />
</UiButton>
</UiTooltip>
@ -25,7 +31,7 @@
<script setup lang="ts">
import { eq } from 'drizzle-orm'
import { type Locale } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
import { haexSettings } from '~~/src-tauri/database/schemas/vault'
definePageMeta({
@ -61,6 +67,7 @@ const onSetVaultNameAsync = async () => {
await updateVaultNameAsync(currentVaultName.value)
add({ text: t('vaultName.update.success'), type: 'success' })
} catch (error) {
console.error(error)
add({ text: t('vaultName.update.error'), type: 'error' })
}
}

View File

@ -1,10 +1,11 @@
import 'flyonui/flyonui'
import type { HSOverlay, IStaticMethods } from 'flyonui/flyonui'
import type { HSOverlay, IStaticMethods, HSAccordion } from 'flyonui/flyonui'
declare global {
interface Window {
HSStaticMethods: IStaticMethods
HSOverlay: typeof HSOverlay
HSAccordion: typeof HSAccordion
}
}

View File

@ -1,38 +1,38 @@
export interface ResourceRequestDetails {
url: string;
resourceType: string;
tabId?: string;
frameId?: number;
url: string
resourceType: string
tabId?: string
frameId?: number
}
export interface ResourceRequestResult {
cancel: boolean;
redirectUrl?: string;
cancel: boolean
redirectUrl?: string
}
export interface ContentScript {
code: string;
matches?: string[];
runAt?: 'document_start' | 'document_end' | 'document_idle';
code: string
matches?: string[]
runAt?: 'document_start' | 'document_end' | 'document_idle'
}
export interface Extension {
id: string;
name: string;
version: string;
description?: string;
processNavigation?: (url: string) => boolean;
id: string
name: string
version: string
description?: string
processNavigation?: (url: string) => boolean
processResourceRequest?: (
details: ResourceRequestDetails
) => ResourceRequestResult;
contentScripts?: ContentScript[];
details: ResourceRequestDetails,
) => ResourceRequestResult
contentScripts?: ContentScript[]
}
export const useBrowserExtensionStore = defineStore(
'useBrowserExtensionStore',
() => {
const extensions = ref<Extension[]>([]);
const isInitialized = ref<boolean>(false);
const extensions = ref<Extension[]>([])
const isInitialized = ref<boolean>(false)
return {
extensions,
@ -40,28 +40,28 @@ export const useBrowserExtensionStore = defineStore(
initializeAsync,
processNavigation,
injectContentScripts,
};
}
);
}
},
)
const initializeAsync = async () => {
const { isInitialized } = storeToRefs(useBrowserExtensionStore());
const { isInitialized } = storeToRefs(useBrowserExtensionStore())
return
if (isInitialized.value) return;
if (isInitialized.value) return
// Lade Erweiterungen aus dem Erweiterungsverzeichnis
try {
const extensions = await loadExtensionsAsync();
const extensions = await loadExtensionsAsync()
for (const extension of extensions) {
registerExtension(extension);
registerExtension(extension)
}
isInitialized.value = true;
console.log(`${extensions.length} Erweiterungen geladen`);
isInitialized.value = true
console.log(`${extensions.length} Erweiterungen geladen`)
} catch (error) {
console.error('Fehler beim Laden der Erweiterungen:', error);
console.error('Fehler beim Laden der Erweiterungen:', error)
}
};
}
const loadExtensionsAsync = async (): Promise<Extension[]> => {
// In einer realen Implementierung würden Sie hier Erweiterungen aus einem Verzeichnis laden
@ -69,23 +69,23 @@ const loadExtensionsAsync = async (): Promise<Extension[]> => {
/* const adBlocker = (await import('./ad-blocker')).default;
const trackerBlocker = (await import('./tracker-blocker')).default; */
return [];
};
return []
}
const registerExtension = (extension: Extension): boolean => {
const { extensions } = storeToRefs(useBrowserExtensionStore());
const { extensions } = storeToRefs(useBrowserExtensionStore())
if (!extension.id || !extension.name) {
console.error('Ungültige Erweiterung:', extension);
return false;
console.error('Ungültige Erweiterung:', extension)
return false
}
console.log(`Erweiterung registriert: ${extension.name}`);
extensions.value.push(extension);
return true;
};
console.log(`Erweiterung registriert: ${extension.name}`)
extensions.value.push(extension)
return true
}
const processNavigation = (url: string) => {
return true;
};
const processNavigation = () => {
return true
}
const injectContentScripts = (t: string) => { };
const injectContentScripts = () => {}

View File

@ -1,116 +1,145 @@
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { appDataDir, join } from "@tauri-apps/api/path";
import { exists, readDir, readTextFile, remove } from "@tauri-apps/plugin-fs";
import { and, eq } from "drizzle-orm";
import { invoke } from '@tauri-apps/api/core'
import { appDataDir, join } from '@tauri-apps/api/path'
import { exists, readDir, readTextFile, remove } from '@tauri-apps/plugin-fs'
import { and, eq } from 'drizzle-orm'
import type {
IHaexHubExtension,
IHaexHubExtensionLink,
IHaexHubExtensionManifest,
} from "~/types/haexhub";
import { haexExtensions } from "~~/src-tauri/database/schemas/vault";
} from '~/types/haexhub'
import { haexExtensions } from '~~/src-tauri/database/schemas/vault'
const manifestFileName = "manifest.json";
const logoFileName = "logo.svg";
const manifestFileName = 'manifest.json'
const logoFileName = 'icon.svg'
export const useExtensionsStore = defineStore("extensionsStore", () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([]);
export const useExtensionsStore = defineStore('extensionsStore', () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([])
const extensionLinks = computed<ISidebarItem[]>(() =>
availableExtensions.value
.filter((extension) => extension.enabled && extension.installed)
.map((extension) => ({
icon: extension.icon ?? "",
icon: extension.icon ?? '',
id: extension.id,
name: extension.name ?? "",
tooltip: extension.name ?? "",
to: { name: "haexExtension", params: { extensionId: extension.id } },
}))
);
name: extension.name ?? '',
tooltip: extension.name ?? '',
to: { name: 'haexExtension', params: { extensionId: extension.id } },
})),
)
const currentRoute = useRouter().currentRoute;
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(() => {
console.log("computed currentExtension", currentRoute.value.params);
if (currentRoute.value.meta.name !== "haexExtension") return;
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;
const extensionId = getSingleRouteParam(
currentRoute.value.params.extensionId,
)
console.log('extensionId from param', extensionId)
if (!extensionId) return
const extension = availableExtensions.value.find((extension) => extension.id === extensionId);
console.log("currentExtension", extension);
return extension;
});
const extension = availableExtensions.value.find(
(extension) => extension.id === extensionId,
)
console.log('currentExtension', extension)
return extension
})
const getExtensionPathAsync = async (extensionId?: string, version?: string) => {
if (!extensionId || !version) return "";
return await join(await appDataDir(), "extensions", extensionId, version);
};
const getExtensionPathAsync = async (
extensionId?: string,
version?: string,
) => {
if (!extensionId || !version) return ''
return await join(await appDataDir(), 'extensions', extensionId, version)
}
const checkSourceExtensionDirectoryAsync = async (extensionDirectory: string) => {
const checkSourceExtensionDirectoryAsync = async (
extensionDirectory: string,
) => {
try {
const dir = await readDir(extensionDirectory);
const manifest = dir.find((entry) => entry.name === manifestFileName && entry.isFile);
if (!manifest) throw new Error("Kein Manifest für Erweiterung gefunden");
const dir = await readDir(extensionDirectory)
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");
const logo = dir.find((item) => item.isFile && item.name === logoFileName)
if (!logo) throw new Error('Logo fehlt')
console.log('found icon', logo)
return true;
return true
} catch (error) {
throw new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
console.error(error)
//throw error //new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
}
};
}
const isExtensionInstalledAsync = async (extension: Partial<IHaexHubExtension>) => {
const isExtensionInstalledAsync = async (
extension: Partial<IHaexHubExtension>,
) => {
try {
const extensionPath = await getExtensionPathAsync(extension.id, `${extension.version}`);
console.log(`extension ${extension.id} is installed ${await exists(extensionPath)}`);
return await exists(extensionPath);
const extensionPath = await getExtensionPathAsync(
extension.id,
`${extension.version}`,
)
console.log(
`extension ${extension.id} is installed ${await exists(extensionPath)}`,
)
return await exists(extensionPath)
} catch (error) {
return false;
console.error(error)
return false
}
};
}
const checkManifest = (manifestFile: unknown): manifestFile is IHaexHubExtensionManifest => {
const errors = [];
const checkManifest = (
manifestFile: unknown,
): manifestFile is IHaexHubExtensionManifest => {
const errors = []
if (typeof manifestFile !== "object" || manifestFile === null) {
errors.push("Manifest ist falsch");
return false;
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 (!('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 (!('name' in manifestFile) || typeof manifestFile.name !== 'string')
errors.push('Name fehlt')
if (!("entry" in manifestFile) || typeof manifestFile.entry !== "string")
errors.push("Entry fehlerhaft");
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 (!('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 (!('url' in manifestFile) || typeof manifestFile.url !== 'string')
errors.push('Url fehlt')
if (
!("permissions" in manifestFile) ||
typeof manifestFile.permissions !== "object" ||
!('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");
errors.push('Berechtigungen fehlen')
}
if (errors.length) throw errors;
if (errors.length) throw errors
/* const permissions = manifestFile.permissions as Partial<IHaexHubExtensionManifest["permissions"]>;
if (
@ -122,59 +151,75 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
return false;
} */
return true;
};
return true
}
const readManifestFileAsync = async (extensionId: string, version: string) => {
const readManifestFileAsync = async (
extensionId: string,
version: string,
) => {
try {
if (!(await isExtensionInstalledAsync({ id: extensionId, version }))) return null;
if (!(await isExtensionInstalledAsync({ id: extensionId, version })))
return null
const extensionPath = await getExtensionPathAsync(extensionId, `${version}`);
const manifestPath = await join(extensionPath, manifestFileName);
const extensionPath = await getExtensionPathAsync(
extensionId,
`${version}`,
)
const manifestPath = await join(extensionPath, manifestFileName)
const manifest = (await JSON.parse(
await readTextFile(manifestPath)
)) as IHaexHubExtensionManifest;
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
/*
TODO implement check, that manifest has valid data
*/
return manifest;
return manifest
} catch (error) {
console.error("ERROR readManifestFileAsync", error);
console.error('ERROR readManifestFileAsync', error)
}
};
}
const installAsync = async (extensionDirectory: string | null, global: boolean = true) => {
const installAsync = async (extensionDirectory: string | null) => {
try {
if (!extensionDirectory) throw new Error("Kein Ordner für Erweiterung angegeben");
const manifestPath = await join(extensionDirectory, manifestFileName);
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;
await readTextFile(manifestPath),
)) as IHaexHubExtensionManifest
const destination = await getExtensionPathAsync(manifest.id, manifest.version);
const destination = await getExtensionPathAsync(
manifest.id,
manifest.version,
)
await checkSourceExtensionDirectoryAsync(extensionDirectory);
await checkSourceExtensionDirectoryAsync(extensionDirectory)
await invoke("copy_directory", { source: extensionDirectory, destination });
await invoke('copy_directory', {
source: extensionDirectory,
destination,
})
const logoFilePath = await join(destination, "logo.svg");
const logoSvg = await readTextFile(logoFilePath);
const logoFilePath = await join(destination, logoFileName)
const logo = 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,
});
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: logo,
})
console.log("insert extensions", res);
console.log('insert extensions', res)
} catch (error) {
throw error;
throw error
/*
const resourcePath = await resourceDir();
//const manifestPath = await join(extensionDirectory, 'manifest.json');
@ -280,7 +325,7 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
console.log(`Plugin ${manifest.name} geladen.`); */
}
};
}
const extensionEntry = computedAsync(
async () => {
@ -289,153 +334,79 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
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;
console.log('extension id or entry missing', currentExtension.value)
return '' // "no mani: " + currentExtension.value;
}
const extensionPath = await getExtensionPathAsync(
currentExtension.value?.id,
currentExtension.value?.version
); //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
currentExtension.value?.version,
) //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
console.log("extensionEntry extensionPath", extensionPath);
console.log('extensionEntry extensionPath', extensionPath)
const manifest = await readManifestFileAsync(
currentExtension.value.id,
currentExtension.value.version
);
currentExtension.value.version,
)
if (!manifest) return ""//"no manifest readable";
if (!manifest) return '' //"no manifest readable";
const entryPath = await join(extensionPath, manifest.entry);
//const entryPath = await join(extensionPath, manifest.entry)
const hexName = stringToHex(
JSON.stringify({
id: currentExtension.value.id,
version: currentExtension.value.version,
})
);
}),
)
return `haex-extension://${hexName}`;
return convertFileSrc(entryPath); //`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 srcFile = convertFileSrc(await join(extensionPath, resource));
entryHtml = entryHtml.replace(match, `${prefix}${srcFile}${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.`); */
return `haex-extension://${hexName}`
} catch (error) {
console.error("ERROR extensionEntry", error);
console.error('ERROR extensionEntry', error)
}
},
null,
{ lazy: true }
);
{ lazy: true },
)
const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore());
const { currentVault } = storeToRefs(useVaultStore())
const extensions = (await currentVault.value?.drizzle.select().from(haexExtensions)) ?? [];
const extensions =
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(extensions, isExtensionInstalledAsync);
console.log("loadExtensionsAsync installedExtensions", installedExtensions);
const installedExtensions = await filterAsync(
extensions,
isExtensionInstalledAsync,
)
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
availableExtensions.value =
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? "",
icon: extension.icon ?? "",
author: extension.author ?? "",
version: extension.version ?? "",
name: extension.name ?? '',
icon: extension.icon ?? '',
author: extension.author ?? '',
version: extension.version ?? '',
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? [];
})) ?? []
console.log("loadExtensionsAsync", availableExtensions.value);
return true;
};
console.log('loadExtensionsAsync', availableExtensions.value)
return true
}
const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log("remove extension", id, version);
await removeExtensionFromVaultAsync(id, version);
await removeExtensionFilesAsync(id, version);
console.log('remove extension', id, version)
await removeExtensionFromVaultAsync(id, version)
await removeExtensionFilesAsync(id, version)
} catch (error) {
throw new Error(JSON.stringify(error));
throw new Error(JSON.stringify(error))
}
};
}
return {
availableExtensions,
@ -445,48 +416,63 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
extensionLinks,
installAsync,
isActive,
isExtensionInstalledAsync,
loadExtensionsAsync,
readManifestFileAsync,
removeExtensionAsync,
getExtensionPathAsync,
};
});
}
})
const getMimeType = (file: string) => {
if (file.endsWith(".css")) return "text/css";
if (file.endsWith(".js")) return "text/javascript";
return "text/plain";
};
/* const getMimeType = (file: string) => {
if (file.endsWith('.css')) return 'text/css'
if (file.endsWith('.js')) return 'text/javascript'
return 'text/plain'
} */
const removeExtensionFromVaultAsync = async (id: string | null, version: string | null) => {
if (!id) throw new Error("Erweiterung kann nicht gelöscht werden. Es keine ID angegeben");
const removeExtensionFromVaultAsync = async (
id: string | null,
version: string | null,
) => {
if (!id)
throw new Error(
'Erweiterung kann nicht gelöscht werden. Es keine ID angegeben',
)
if (!version)
throw new Error("Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben");
throw new Error(
'Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben',
)
const { currentVault } = useVaultStore();
const { currentVault } = useVaultStore()
const removedExtensions = await currentVault?.drizzle
.delete(haexExtensions)
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)));
return removedExtensions;
};
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
return removedExtensions
}
const removeExtensionFilesAsync = async (id: string | null, version: string | null) => {
const removeExtensionFilesAsync = async (
id: string | null,
version: string | null,
) => {
try {
const { getExtensionPathAsync } = useExtensionsStore();
if (!id) throw new Error("Erweiterung kann nicht gelöscht werden. Es keine ID angegeben");
const { getExtensionPathAsync } = useExtensionsStore()
if (!id)
throw new Error(
'Erweiterung kann nicht gelöscht werden. Es keine ID angegeben',
)
if (!version)
throw new Error("Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben");
throw new Error(
'Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben',
)
const extensionDirectory = await getExtensionPathAsync(id, version);
const extensionDirectory = await getExtensionPathAsync(id, version)
await remove(extensionDirectory, {
recursive: true,
});
})
} catch (error) {
console.error("ERROR removeExtensionFilesAsync", error);
throw new Error(JSON.stringify(error));
console.error('ERROR removeExtensionFilesAsync', error)
throw new Error(JSON.stringify(error))
}
};
const replaceUrlWithAssetProtocolAsync = () => { };
}

View File

@ -1,9 +1,10 @@
export interface IHaexNotication {
title: string;
description?: string;
icon?: string;
image?: string;
alt?: string;
title: string
description?: string
icon?: string
image?: string
alt?: string
date: Date
}
export const useNotificationStore = defineStore('notificationStore', () => {
@ -13,10 +14,11 @@ export const useNotificationStore = defineStore('notificationStore', () => {
alt: 'test',
description: 'Ganz was tolles',
image: 'https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png',
date: new Date(),
},
]);
])
return {
notifications,
};
});
}
})

View File

@ -1,25 +1,31 @@
import type { RouteLocationAsRelativeGeneric } from "vue-router";
import type { RouteLocationAsRelativeGeneric } from 'vue-router'
export interface ISidebarItem {
name: string;
icon: string;
tooltip?: string;
id: string;
to?: RouteLocationAsRelativeGeneric;
iconType?: "icon" | "svg";
name: string
icon: string
tooltip?: string
id: string
to?: RouteLocationAsRelativeGeneric
iconType?: 'icon' | 'svg'
}
export const useSidebarStore = defineStore("sidebarStore", () => {
const isVisible = ref(true);
export const useSidebarStore = defineStore('sidebarStore', () => {
const isVisible = ref(true)
const menu = ref<ISidebarItem[]>([
{
id: "haex-extensions-add",
name: "Haex Extensions",
icon: "gg:extension",
to: { name: "extensionOverview" },
id: 'haex-pass',
name: 'HaexPass',
icon: 'mdi:safe',
to: { name: 'haexpassOverview' },
},
]);
{
id: 'haex-extensions',
name: 'Haex Extensions',
icon: 'gg:extension',
to: { name: 'extensionOverview' },
},
])
/* const loadAsync = async (id: string) => {
extensions.value.some(async (extension) => {
@ -36,5 +42,5 @@ export const useSidebarStore = defineStore("sidebarStore", () => {
menu,
isVisible,
//loadAsync,
};
});
}
})

View File

@ -1,66 +1,76 @@
import * as schema from "@/../src-tauri/database/schemas/vault";
import { invoke } from "@tauri-apps/api/core";
import { platform } from "@tauri-apps/plugin-os";
import { eq } from "drizzle-orm";
import { drizzle, SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
import * as schema from '@/../src-tauri/database/schemas/vault'
import { invoke } from '@tauri-apps/api/core'
import { platform } from '@tauri-apps/plugin-os'
import { eq } from 'drizzle-orm'
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'
import { drizzle } from 'drizzle-orm/sqlite-proxy'
interface IVault {
name: string;
drizzle: SqliteRemoteDatabase<typeof schema>;
name: string
drizzle: SqliteRemoteDatabase<typeof schema>
}
interface IOpenVaults {
[vaultId: string]: IVault;
[vaultId: string]: IVault
}
export const useVaultStore = defineStore("vaultStore", () => {
export const useVaultStore = defineStore('vaultStore', () => {
const currentVaultId = computed<string | undefined>({
get: () => getSingleRouteParam(useRouter().currentRoute.value.params.vaultId),
get: () =>
getSingleRouteParam(useRouter().currentRoute.value.params.vaultId),
set: (newVaultId) => {
useRouter().currentRoute.value.params.vaultId = newVaultId ?? "";
useRouter().currentRoute.value.params.vaultId = newVaultId ?? ''
},
});
})
const defaultVaultName = ref("HaexHub")
const defaultVaultName = ref('HaexHub')
const currentVaultName = ref(defaultVaultName.value)
const read_only = computed<boolean>({
get: () => {
console.log("query showSidebar", useRouter().currentRoute.value.query.readonly);
console.log(
'query showSidebar',
useRouter().currentRoute.value.query.readonly,
)
return JSON.parse(
getSingleRouteParam(useRouter().currentRoute.value.query.readonly) || "false"
);
getSingleRouteParam(useRouter().currentRoute.value.query.readonly) ||
'false',
)
},
set: (readonly) => {
const router = useRouter();
const router = useRouter()
router.replace({
query: {
...router.currentRoute.value.query,
readonly: JSON.stringify(readonly ? true : false),
},
});
})
},
});
})
const openVaults = ref<IOpenVaults>({});
const openVaults = ref<IOpenVaults>({})
const currentVault = computed(() => openVaults.value?.[currentVaultId.value ?? ""])
const currentVault = computed(
() => openVaults.value?.[currentVaultId.value ?? ''],
)
const openAsync = async ({ path = "", password }: { path: string; password: string }) => {
const openAsync = async ({
path = '',
password,
}: {
path: string
password: string
}) => {
try {
const result = await invoke<string>("open_encrypted_database", {
const result = await invoke<string>('open_encrypted_database', {
path,
key: password,
});
})
if (result !== "success") throw new Error(result);
if (result !== 'success') throw new Error(result)
const vaultId = await getVaultIdAsync(path);
const seperator = platform() === "windows" ? "\\" : "/";
const fileName = path.split(seperator).pop();
const vaultId = await getVaultIdAsync(path)
const seperator = platform() === 'windows' ? '\\' : '/'
const fileName = path.split(seperator).pop()
openVaults.value = {
...openVaults.value,
@ -68,53 +78,65 @@ export const useVaultStore = defineStore("vaultStore", () => {
name: fileName ?? path,
drizzle: drizzle<typeof schema>(
async (sql, params: unknown[], method) => {
let rows: any = [];
let results = [];
let rows: unknown[] = []
let results: any = []
// 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);
console.log('sql_select', sql, params)
rows = await invoke<unknown[]>('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: [] };
rows = await invoke<unknown[]>('sql_execute', {
sql,
params,
}).catch((e) => {
console.error('SQL execute Error:', e, sql, params)
return []
})
return { rows: [] }
}
results = method === "all" ? rows : rows[0];
results = method === 'all' ? rows : rows[0]
return { rows: results };
return { rows: results }
},
{ schema: schema, logger: true }
{ schema: schema, logger: true },
),
},
};
}
const { addVaultAsync } = useLastVaultStore();
const { addVaultAsync } = useLastVaultStore()
await addVaultAsync({ path })
return vaultId;
return vaultId
} catch (error) {
console.error("Error openAsync ", error);
return false;
console.error('Error openAsync ', error)
return false
}
};
}
const refreshDatabaseAsync = async () => {
console.log("refreshDatabaseAsync");
console.log('refreshDatabaseAsync')
/* if (!currentVault.value?.database.close) {
return navigateTo(useLocaleRoute()({ name: 'vaultOpen' }));
} */
};
}
const createAsync = async ({ path, password }: { path: string; password: string }) => {
const createAsync = async ({
path,
password,
}: {
path: string
password: string
}) => {
/* const existDb = await exists('default.db', {
baseDir: BaseDirectory.Resource,
}); */
@ -122,16 +144,16 @@ export const useVaultStore = defineStore("vaultStore", () => {
/* const existDb = await resolveResource('resources/default.db');
if (!existDb) throw new Error('Keine Datenbank da');
await copyFile(existDb, path); */
const result = await invoke("create_encrypted_database", {
const result = await invoke('create_encrypted_database', {
path,
key: password,
});
console.log("create_encrypted_database", result);
return await openAsync({ path, password });
};
})
console.log('create_encrypted_database', result)
return await openAsync({ path, password })
}
const closeAsync = async () => {
if (!currentVaultId.value) return;
if (!currentVaultId.value) return
/* if (
typeof openVaults.value?.[currentVaultId.value]?.database?.close ===
@ -140,8 +162,8 @@ export const useVaultStore = defineStore("vaultStore", () => {
console.log('close db', openVaults.value?.[currentVaultId.value]);
return openVaults.value?.[currentVaultId.value]?.database?.close();
} */
delete openVaults.value?.[currentVaultId.value];
};
delete openVaults.value?.[currentVaultId.value]
}
const syncLocaleAsync = async () => {
try {
@ -154,21 +176,25 @@ export const useVaultStore = defineStore("vaultStore", () => {
if (currentLocaleRow?.[0]?.value) {
const currentLocale = app.$i18n.availableLocales.find(
(locale) => locale === currentLocaleRow[0].value
(locale) => locale === currentLocaleRow[0].value,
)
await app.$i18n.setLocale(currentLocale ?? app.$i18n.defaultLocale)
} else {
await currentVault.value?.drizzle
.insert(schema.haexSettings)
.values({ id: crypto.randomUUID(), key: 'locale', value: app.$i18n.locale.value })
await currentVault.value?.drizzle.insert(schema.haexSettings).values({
id: crypto.randomUUID(),
key: 'locale',
value: app.$i18n.locale.value,
})
}
} catch (error) {
console.log("ERROR syncLocaleAsync", error)
console.log('ERROR syncLocaleAsync', error)
}
}
const syncThemeAsync = async () => {
const { availableThemes, defaultTheme, currentTheme } = storeToRefs(useUiStore())
const { availableThemes, defaultTheme, currentTheme } = storeToRefs(
useUiStore(),
)
const currentThemeRow = await currentVault.value?.drizzle
.select()
.from(schema.haexSettings)
@ -176,7 +202,7 @@ export const useVaultStore = defineStore("vaultStore", () => {
if (currentThemeRow?.[0]?.value) {
const theme = availableThemes.value.find(
(theme) => theme.name === currentThemeRow[0].value
(theme) => theme.name === currentThemeRow[0].value,
)
currentTheme.value = theme ?? defaultTheme.value
} else {
@ -195,7 +221,8 @@ export const useVaultStore = defineStore("vaultStore", () => {
.where(eq(schema.haexSettings.key, 'vaultName'))
if (currentVaultNameRow?.[0]?.value) {
currentVaultName.value = currentVaultNameRow.at(0)?.value ?? defaultVaultName.value
currentVaultName.value =
currentVaultNameRow.at(0)?.value ?? defaultVaultName.value
} else {
await currentVault.value?.drizzle.insert(schema.haexSettings).values({
id: crypto.randomUUID(),
@ -206,8 +233,11 @@ export const useVaultStore = defineStore("vaultStore", () => {
}
const updateVaultNameAsync = async (newVaultName?: string | null) => {
console.log("set new vaultName", newVaultName)
return currentVault.value?.drizzle.update(schema.haexSettings).set({ value: newVaultName ?? defaultVaultName.value }).where(eq(schema.haexSettings.key, "vaultName"))
console.log('set new vaultName', newVaultName)
return currentVault.value?.drizzle
.update(schema.haexSettings)
.set({ value: newVaultName ?? defaultVaultName.value })
.where(eq(schema.haexSettings.key, 'vaultName'))
}
return {
@ -224,24 +254,21 @@ export const useVaultStore = defineStore("vaultStore", () => {
syncThemeAsync,
syncVaultNameAsync,
updateVaultNameAsync,
};
});
}
})
const getVaultIdAsync = async (path: string) => {
const encoder = new TextEncoder();
const data = encoder.encode(path);
const encoder = new TextEncoder()
const data = encoder.encode(path)
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); // convert bytes to hex string
console.log("vaultId", hashHex);
return hashHex;
};
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
console.log('vaultId', hashHex)
return hashHex
}
const isSelectQuery = (sql: string) => {
const selectRegex = /^\s*SELECT\b/i;
return selectRegex.test(sql);
};
const selectRegex = /^\s*SELECT\b/i
return selectRegex.test(sql)
}

View File

@ -1,63 +1,63 @@
import { load, Store } from '@tauri-apps/plugin-store';
import { load } from '@tauri-apps/plugin-store'
interface ILastVault {
lastUsed: Date;
name: string;
path: string;
lastUsed: Date
name: string
path: string
}
export const useLastVaultStore = defineStore('lastVaultStore', () => {
const {
public: { haexVault },
} = useRuntimeConfig();
} = useRuntimeConfig()
const lastVaults = ref<ILastVault[]>([]);
const lastVaults = ref<ILastVault[]>([])
const keyName = 'lastVaults';
const keyName = 'lastVaults'
const getStoreAsync = async () => {
return await load(haexVault.lastVaultFileName);
};
return await load(haexVault.lastVaultFileName)
}
const syncLastVaultsAsync = async () => {
const store = await getStoreAsync();
const store = await getStoreAsync()
lastVaults.value =
(await store.get<ILastVault[]>(keyName))?.sort(
(a, b) => +new Date(b.lastUsed) - +new Date(a.lastUsed)
) ?? [];
(a, b) => +new Date(b.lastUsed) - +new Date(a.lastUsed),
) ?? []
return lastVaults.value;
};
return lastVaults.value
}
const addVaultAsync = async ({
name,
path,
}: {
name?: string;
path: string;
name?: string
path: string
}) => {
if (!lastVaults.value) await syncLastVaultsAsync();
if (!lastVaults.value) await syncLastVaultsAsync()
const saveName = name || getFileNameFromPath(path);
lastVaults.value = lastVaults.value.filter((vault) => vault.path !== path);
lastVaults.value.push({ lastUsed: new Date(), name: saveName, path });
await saveLastVaultsAsync();
};
const saveName = name || getFileNameFromPath(path)
lastVaults.value = lastVaults.value.filter((vault) => vault.path !== path)
lastVaults.value.push({ lastUsed: new Date(), name: saveName, path })
await saveLastVaultsAsync()
}
const removeVaultAsync = async (vaultPath: string) => {
console.log('remove', vaultPath, lastVaults.value);
console.log('remove', vaultPath, lastVaults.value)
lastVaults.value = lastVaults.value.filter(
(vault) => vault.path !== vaultPath
);
await saveLastVaultsAsync();
};
(vault) => vault.path !== vaultPath,
)
await saveLastVaultsAsync()
}
const saveLastVaultsAsync = async () => {
const store = await getStoreAsync();
console.log('save lastVaults', keyName, lastVaults.value);
await store.set(keyName, lastVaults.value);
await syncLastVaultsAsync();
};
const store = await getStoreAsync()
console.log('save lastVaults', keyName, lastVaults.value)
await store.set(keyName, lastVaults.value)
await syncLastVaultsAsync()
}
return {
addVaultAsync,
@ -65,16 +65,16 @@ export const useLastVaultStore = defineStore('lastVaultStore', () => {
lastVaults,
removeVaultAsync,
saveLastVaultsAsync,
};
});
}
})
const getFileNameFromPath = (path: string) => {
const lastBackslashIndex = path.lastIndexOf('\\');
const lastSlashIndex = path.lastIndexOf('/');
const lastBackslashIndex = path.lastIndexOf('\\')
const lastSlashIndex = path.lastIndexOf('/')
const lastIndex = Math.max(lastBackslashIndex, lastSlashIndex);
const lastIndex = Math.max(lastBackslashIndex, lastSlashIndex)
const fileName = path.substring(lastIndex + 1);
const fileName = path.substring(lastIndex + 1)
return fileName;
};
return fileName
}

View File

@ -1,35 +1,35 @@
export interface IHaexHubExtensionManifest {
name: string;
id: string;
entry: string;
author: string;
url: string;
version: string;
icon: string;
name: string
id: string
entry: string
author: string
url: string
version: string
icon: string
permissions: {
database?: {
read?: string[];
write?: string[];
create?: string[];
};
http?: string[];
read?: string[]
write?: string[]
create?: string[]
}
http?: string[]
filesystem?: {
read?: string[];
write?: string[];
};
};
read?: string[]
write?: string[]
}
}
}
export interface IHaexHubExtensionLink extends IHaexHubExtension {
installed: boolean;
installed: boolean
}
export interface IHaexHubExtension {
author?: string | null;
enabled?: boolean | null;
icon?: string | null;
id: string;
manifest?: IHaexHubExtensionManifest;
name: string | null;
version?: string | null;
author?: string | null
enabled?: boolean | null
icon?: string | null
id: string
manifest?: IHaexHubExtensionManifest
name: string | null
version?: string | null
}

View File

@ -1,3 +1,5 @@
import type { LocationQueryValue, RouteLocationRawI18n } from 'vue-router'
export const bytesToBase64DataUrlAsync = async (
bytes: Uint8Array,
type = 'application/octet-stream'
@ -15,7 +17,7 @@ export const blobToImageAsync = (blob: Blob): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
console.log('transform blob', blob)
const url = URL.createObjectURL(blob)
let img = new Image()
const img = new Image()
img.onload = () => {
URL.revokeObjectURL(url)
resolve(img)
@ -62,8 +64,6 @@ export const readableFileSize = (sizeInByte: number | string = 0) => {
return `${sizeInKb.toFixed(2)} KB`
}
import type { LocationQueryValue, RouteLocationRawI18n } from 'vue-router'
export const getSingleRouteParam = (
param: string | string[] | LocationQueryValue | LocationQueryValue[]
): string => {

View File

@ -1,57 +0,0 @@
(function () {
// Überwache das Erstellen von Elementen, die Ressourcen laden
const originalCreateElement = document.createElement;
document.createElement = function (tagName) {
const element = originalCreateElement.call(document, tagName);
if (
tagName.toLowerCase() === 'img' ||
tagName.toLowerCase() === 'script' ||
tagName.toLowerCase() === 'iframe'
) {
// Überwache das Setzen des src-Attributs
const originalSetAttribute = element.setAttribute;
element.setAttribute = function (name, value) {
if (name === 'src') {
// Prüfe, ob die Ressource blockiert werden soll
window.__TAURI__
.invoke('block_resource_request', {
url: value,
resourceType: tagName.toLowerCase(),
})
.then((shouldBlock) => {
if (shouldBlock) {
console.log(`Ressourcenanfrage blockiert: ${value}`);
return;
}
originalSetAttribute.call(element, name, value);
});
} else {
originalSetAttribute.call(element, name, value);
}
};
}
return element;
};
// Wenn die Tauri HTTP API verwendet wird, können wir sie hier überwachen
if (window.__TAURI__ && window.__TAURI__.http) {
const originalFetch = window.__TAURI__.http.fetch;
window.__TAURI__.http.fetch = async function (options) {
// Prüfe, ob die Ressource blockiert werden soll
const shouldBlock = await invoke('block_resource_request', {
url: options.url,
resourceType: 'tauri-fetch',
});
if (shouldBlock) {
throw new Error(`Ressourcenanfrage blockiert: ${options.url}`);
}
return originalFetch.call(this, options);
};
}
console.log('Webview-Bridge geladen');
})();