switch to nuxt ui

This commit is contained in:
2025-09-11 00:58:55 +02:00
parent 3975d26caa
commit 0a7de8b78b
143 changed files with 19019 additions and 9899 deletions

View File

@ -1,156 +0,0 @@
<template>
<div class="browser">
<div class="browser-controls">
<button :disabled="!activeTabId" @click="$emit('goBack', activeTabId)">
</button>
<button :disabled="!activeTabId" @click="$emit('goForward', activeTabId)">
</button>
<button @click="$emit('createTab')">+</button>
<HaexBrowserUrlBar
:url="activeTab?.url || ''"
:is-loading="activeTab?.isLoading || false"
@submit="handleUrlSubmit"
/>
</div>
<HaexBrowserTabBar
:tabs="tabs"
:active-tab-id="activeTabId"
@close-tab="$emit('closeTab', $event)"
@activate-tab="$emit('activateTab', $event)"
/>
<div ref="contentRef" class="browser-content">
<!-- Die eigentlichen Webview-Inhalte werden von Tauri verwaltet -->
<div v-if="!activeTabId" class="empty-state">
<p>
Kein Tab geöffnet. Erstellen Sie einen neuen Tab mit dem + Button.
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Webview } from '@tauri-apps/api/webview'
import { Window } from '@tauri-apps/api/window'
/* const appWindow = new Window('uniqueLabel');
const webview = new Webview(appWindow, 'theUniqueLabel', {
url: 'https://www.google.de',
x: 0,
y: 0,
height: 1000,
width: 1000,
});
webview.once('tauri://created', function () {
console.log('create new webview');
}); */
interface Tab {
id: string
title: string
url: string
isLoading: boolean
isActive: boolean
window_label: string
}
interface Props {
tabs: Tab[]
activeTabId: string | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'createTab'): void
(e: 'closeTab', tabId: string): void
(e: 'navigate', tabId: string, url: string): void
(e: 'goBack', tabId: string | null): void
(e: 'goForward', tabId: string | null): void
(e: 'activateTab', tabId: string | null): void
}>()
const { initializeAsync, processNavigation, injectContentScripts } =
useBrowserExtensionStore()
const contentRef = ref<HTMLDivElement | null>(null)
//const extensionManager = ref<ExtensionManager>(new ExtensionManager());
const activeTab = computed(() =>
props.tabs?.find((tab) => tab.id === props.activeTabId)
)
onMounted(async () => {
// Initialisiere das Erweiterungssystem
await initializeAsync()
// Aktualisiere die Webview-Größe
await updateWebviewBoundsAsync()
//window.addEventListener('resize', updateWebviewBounds);
})
// Wenn ein neuer Tab aktiviert wird, injiziere Content-Scripts
/* watch(
() => props.activeTabId,
async (newTabId) => {
if (newTabId && props.tabs.length > 0) {
const activeTab = props.tabs.find((tab) => tab.id === newTabId);
if (activeTab) {
// Warte kurz, bis die Seite geladen ist
setTimeout(() => {
injectContentScripts(activeTab.window_label);
}, 500);
// Aktualisiere die Webview-Größe
updateWebviewBounds();
}
}
}
); */
const createNewTabAsync = async () => {
const appWindow = new Window(crypto.randomUUID())
appWindow.setAlwaysOnTop(true)
appWindow.setDecorations(false)
const webview = new Webview(appWindow, 'theUniqueLabel', {
url: 'https://www.google.de',
x: 0,
y: 0,
height: 1000,
width: 1000,
})
}
const handleUrlSubmit = (url: string) => {
createNewTabAsync()
if (props.activeTabId) {
// Prüfe URL mit Erweiterungen vor der Navigation
/* if (processNavigation(url)) {
//emit('navigate', props.activeTabId, url);
} else {
console.log('Navigation blockiert durch Erweiterung')
// Hier könnten Sie eine Benachrichtigung anzeigen
} */
}
}
const updateWebviewBoundsAsync = async () => {
if (!contentRef.value) return
const rect = contentRef.value.getBoundingClientRect()
const bounds = {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
}
/* await invoke('update_window_bounds', {
contentBounds: { x: bounds.x, y: bounds.y },
contentSize: { width: bounds.width, height: bounds.height },
}); */
}
</script>

View File

@ -1,40 +0,0 @@
<template>
<div class="tab-bar">
<div
v-for="tab in tabs"
:key="tab.id"
class="tab"
:class="{ active: tab.id === activeTabId }"
@click="$emit('activateTab', tab.id)"
>
<span class="tab-title">
{{ tab.title || 'Neuer Tab' }}
</span>
<button class="tab-close" @click.stop="$emit('closeTab', tab.id)">
×
</button>
</div>
</div>
</template>
<script setup lang="ts">
interface Tab {
id: string
title: string
url: string
isLoading: boolean
isActive: boolean
}
interface Props {
tabs: Tab[]
activeTabId: string | null
}
defineProps<Props>()
defineEmits<{
(e: 'closeTab', tabId: string): void
(e: 'activateTab', tabId: string): void
}>()
</script>

View File

@ -1,41 +0,0 @@
<template>
<form class="url-bar" @submit.prevent="handleSubmit">
<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>
</template>
<script setup lang="ts">
const props = defineProps({
url: {
type: String,
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['submit'])
const inputValue = ref(props.url)
watch(
() => props.url,
(newUrl) => {
inputValue.value = newUrl
}
)
const handleSubmit = () => {
// URL validieren und ggf. Protokoll hinzufügen
let processedUrl = inputValue.value.trim()
if (processedUrl && !processedUrl.match(/^[a-zA-Z]+:\/\//)) {
processedUrl = 'https://' + processedUrl
}
emit('submit', processedUrl)
}
</script>

View File

@ -1,14 +0,0 @@
<template>
<button class="btn join-item" :type>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps({
type: {
type: String as PropType<"reset" | "submit" | "button">,
default: "button",
},
});
</script>

View File

@ -1,16 +0,0 @@
<template>
<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
)
">
<span class="btn-content inline-flex size-full items-center justify-center px-4 py-2 gap-2">
<slot />
</span>
</button>
</template>
<script setup lang="ts">
import { cn } from "@/lib/utils";
const { className = "primary" } = defineProps<{ className?: string }>()
</script>

View File

@ -1,7 +1,8 @@
<template>
<div
class="card border-4 shadow-md shadow-accent h-48 w-48 overflow-hidden hover:shadow-xl transition-shadow "
v-bind="$attrs">
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">
<UiDropdown class="btn btn-sm btn-text btn-circle">
<template #activator>
@ -9,27 +10,44 @@ class="card border-4 shadow-md shadow-accent h-48 w-48 overflow-hidden hover:s
</template>
<template #items>
<UiButton class="btn-error btn-outline btn-sm " @click="showRemoveDialog = true">
<Icon name="mdi:trash" /> {{ t("remove") }}
<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">
<h5 v-if="name" class="card-title">
<h5
v-if="name"
class="card-title"
>
{{ name }}
</h5>
</div>
<div
class="card-body relative cursor-pointer"
@click="navigateTo(useLocalePath()({ name: 'haexExtension', params: { extensionId: id } }))">
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 class="size-20 absolute bottom-2 right-2" v-html="icon" />
<div
class="size-20 absolute bottom-2 right-2"
v-html="icon"
/>
</div>
<!-- <div class="card-footer">
@ -37,63 +55,69 @@ class="card-body relative cursor-pointer"
</div> -->
</div>
<HaexExtensionDialogRemove v-model:open="showRemoveDialog" :extension @confirm="removeExtensionAsync" />
<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"]);
import type { IHaexHubExtension } from '~/types/haexhub'
const emit = defineEmits(['close', 'submit', 'remove'])
const extension = defineProps<IHaexHubExtension>();
const extension = defineProps<IHaexHubExtension>()
const { escape, enter } = useMagicKeys();
const { escape, enter } = useMagicKeys()
watchEffect(async () => {
if (escape.value) {
await nextTick();
emit("close");
if (escape?.value) {
await nextTick()
emit('close')
}
});
})
watchEffect(async () => {
if (enter.value) {
await nextTick();
emit("submit");
if (enter?.value) {
await nextTick()
emit('submit')
}
});
})
const showRemoveDialog = ref(false)
const { add } = useSnackbar()
const { add } = useToast()
const { t } = useI18n()
const extensionStore = useExtensionsStore()
const removeExtensionAsync = async () => {
if (!extension?.id || !extension?.version) {
add({ type: 'error', text: 'Erweiterung kann nicht gelöscht werden' })
add({
color: 'error',
description: 'Erweiterung kann nicht gelöscht werden',
})
return
}
try {
await extensionStore.removeExtensionAsync(
extension.id,
extension.version
)
await extensionStore.removeExtensionAsync(extension.id, extension.version)
await extensionStore.loadExtensionsAsync()
add({
type: 'success',
color: 'success',
title: t('extension.remove.success.title', {
extensionName: extension.name,
}),
text: t('extension.remove.success.text', {
description: t('extension.remove.success.text', {
extensionName: extension.name,
}),
})
} catch (error) {
add({
type: 'error',
color: 'error',
title: t('extension.remove.error.title'),
text: t('extension.remove.error.text', { error: JSON.stringify(error) }),
description: t('extension.remove.error.text', {
error: JSON.stringify(error),
}),
})
}
}
@ -111,7 +135,6 @@ de:
text: "Erweiterung {extensionName} konnte nicht entfernt werden. \n {error}"
title: 'Fehler beim Entfernen von {extensionName}'
en:
remove: Remove
extension:
@ -122,6 +145,4 @@ en:
error:
text: "Extension {extensionName} couldn't be removed. \n {error}"
title: 'Exception during uninstall {extensionName}'
</i18n>

View File

@ -1,7 +1,14 @@
<template>
<UiDialogConfirm v-model:open="open" @abort="onDeny" @confirm="onConfirm">
<UiDialogConfirm
v-model:open="open"
@abort="onDeny"
@confirm="onConfirm"
>
<template #title>
<i18n-t keypath="question" tag="p">
<i18n-t
keypath="question"
tag="p"
>
<template #extension>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
@ -9,94 +16,138 @@
</template>
<div class="flex flex-col">
<nav class="tabs tabs-bordered" aria-label="Tabs" role="tablist" aria-orientation="horizontal">
<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") }}
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") }}
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") }}
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
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
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">
<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";
import type { IHaexHubExtensionManifest } from '~/types/haexhub'
const { t } = useI18n();
const { t } = useI18n()
const open = defineModel<boolean>("open", { default: false });
const { manifest } = defineProps<{ manifest?: IHaexHubExtensionManifest | null }>();
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
read: manifest?.permissions.database?.read?.map((read) => ({
[read]: true,
})),
write: manifest?.permissions.database?.read?.map(write => ({
[write]: true
write: manifest?.permissions.database?.read?.map((write) => ({
[write]: true,
})),
create: manifest?.permissions.database?.read?.map(create => ({
[create]: true
create: manifest?.permissions.database?.read?.map((create) => ({
[create]: true,
})),
},
filesystem: {
read: manifest?.permissions.filesystem?.read?.map(read => ({
[read]: true
read: manifest?.permissions.filesystem?.read?.map((read) => ({
[read]: true,
})),
write: manifest?.permissions.filesystem?.write?.map(write => ({
[write]: true
write: manifest?.permissions.filesystem?.write?.map((write) => ({
[write]: true,
})),
},
http: manifest?.permissions.http?.map(http => ({
[http]: true
http: manifest?.permissions.http?.map((http) => ({
[http]: true,
})),
}))
watch(permissions, () => console.log("permissions", permissions.value))
const emit = defineEmits(["deny", "confirm"]);
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");
};
open.value = false
console.log('onDeny open', open.value)
emit('deny')
}
const onConfirm = () => {
open.value = false;
console.log("onConfirm open", open.value);
emit("confirm");
};
open.value = false
console.log('onConfirm open', open.value)
emit('confirm')
}
</script>
<i18n lang="json">{
<i18n lang="json">
{
"de": {
"title": "Erweiterung hinzufügen",
"question": "Erweiterung {extension} hinzufügen?",
@ -115,4 +166,5 @@ const onConfirm = () => {
"http": "Internet",
"filesystem": "Filesystem"
}
}</i18n>
}
</i18n>

View File

@ -0,0 +1,56 @@
<template>
<UPopover v-model:open="open">
<UButton
icon="material-symbols:apps"
color="neutral"
variant="outline"
/>
<template #content>
<ul
class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll"
@click="open = false"
>
<UiButton
v-for="item in menu"
:key="item.id"
square
size="xl"
variant="ghost"
:ui="{
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
leadingIcon: 'size-10',
label: 'w-full',
}"
:icon="item.icon"
:label="item.name"
:tooltip="item.name"
@click="item.onSelect"
/>
<!-- <UiButton
v-for="item in extensionLinks"
:key="item.id"
v-bind="item"
icon-type="svg"
/> -->
</ul>
</template>
</UPopover>
</template>
<script setup lang="ts">
//const { extensionLinks } = storeToRefs(useExtensionsStore())
const { menu } = storeToRefs(useSidebarStore())
const open = ref(false)
</script>
<i18n lang="yaml">
de:
settings: 'Einstellungen'
close: 'Vault schließen'
en:
settings: 'Settings'
close: 'Close Vault'
</i18n>

View File

@ -1,63 +0,0 @@
<template>
<UiDropdown offset="[--offset:20]">
<template #activator>
<div
class="size-9.5 rounded-full items-center justify-center text-base-content text-base"
>
<Icon
name="mdi:format-list-bulleted"
class="size-full p-2"
/>
</div>
</template>
<!-- <ul class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60" role="menu" aria-orientation="vertical"
aria-labelledby="dropdown-avatar"> -->
<template #items>
<li>
<NuxtLinkLocale
class="dropdown-item"
:to="{ name: 'settings' }"
>
<span class="icon-[tabler--settings]" />
{{ t('settings') }}
</NuxtLinkLocale>
</li>
<li class="dropdown-footer gap-2">
<button
class="btn btn-error btn-soft btn-block"
@click="onVaultCloseAsync"
>
<span class="icon-[tabler--logout]" />
{{ t('vault.close') }}
</button>
</li>
</template>
<!--
</ul> -->
</UiDropdown>
</template>
<script setup lang="ts">
const { t } = useI18n()
const { closeAsync } = useVaultStore()
const onVaultCloseAsync = async () => {
await closeAsync()
await navigateTo(useLocalePath()({ name: 'vaultOpen' }))
}
</script>
<i18n lang="yaml">
de:
settings: 'Einstellungen'
vault:
close: 'Vault schließen'
en:
settings: 'Settings'
vault:
close: 'Close Vault'
</i18n>

View File

@ -1,90 +0,0 @@
<template>
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:18] [--placement:bottom]"
>
<UiTooltip :tooltip="t('notifications.label')">
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle btn btn-text btn-circle dropdown-open:bg-base-content/10"
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 class="icon-[tabler--bell] text-base-content size-[1.375rem]" />
</div>
</button>
</UiTooltip>
<div
class="dropdown-menu dropdown-open:opacity-100 hidden w-full max-w-96 shadow"
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"
>
<div
v-for="notification in notifications"
:key="notification.id"
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"
/>
</div>
</div>
<div class="w-60">
<h6 class="truncate text-base">
{{ notification.title }}
</h6>
<small class="text-base-content/50 truncate">
{{ notification.text }}
</small>
</div>
</div>
</div>
<NuxtLinkLocale
:to="{ name: 'notifications' }"
class="dropdown-footer justify-center gap-1 hover:bg-base-content/10"
>
<span class="icon-[tabler--eye] size-4" />
{{ t('notifications.view_all') }}
</NuxtLinkLocale>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const { notifications } = storeToRefs(useNotificationStore())
</script>
<i18n lang="yaml">
de:
notifications:
label: Benachrichtigungen
view_all: Alle ansehen
en:
notifications:
label: Notifications
view_all: View all
</i18n>

View File

@ -6,9 +6,11 @@
@abort="$emit('abort')"
@confirm="$emit('confirm')"
>
{{
final ? t('final.question', { itemName }) : t('question', { itemName })
}}
<template #body>
{{
final ? t('final.question', { itemName }) : t('question', { itemName })
}}
</template>
</UiDialogConfirm>
</template>

View File

@ -1,12 +1,16 @@
<template>
<UiDialogConfirm
v-model:open="showUnsavedChangesDialog"
:confirm-label="t('label')"
:title="t('title')"
@abort="$emit('abort')"
@confirm="onConfirm"
v-model:open="showUnsavedChangesDialog"
>
{{ t('question') }}
<template #body>
<div class="flex items-center h-full">
{{ t('question') }}
</div>
</template>
</UiDialogConfirm>
</template>
@ -43,5 +47,5 @@ de:
en:
title: Unsaved changes
question: Should the changes be discarded?
label: discard
label: Discard
</i18n>

View File

@ -1,48 +1,46 @@
<template>
<div class="breadcrumbs">
<ul>
<li>
<NuxtLinkLocale :to="{ name: 'passwordGroupItems' }">
<Icon
name="mdi:safe"
size="24"
/>
</NuxtLinkLocale>
</li>
<template v-for="item in items">
<li class="breadcrumbs-separator rtl:rotate-180">
<Icon name="tabler:chevron-right" />
</li>
<ul class="flex items-center gap-2 p-2">
<li>
<NuxtLinkLocale :to="{ name: 'passwordGroupItems' }">
<Icon
name="mdi:safe"
size="24"
/>
</NuxtLinkLocale>
</li>
<li>
<NuxtLinkLocale
:to="{ name: 'passwordGroupItems', params: { groupId: item.id } }"
>
{{ item.name }}
</NuxtLinkLocale>
</li>
</template>
<li class="ml-2">
<UiTooltip
:tooltip="t('edit')"
class="[--placement:bottom]"
<li
v-for="item in items"
:key="item.id"
class="flex items-center gap-2"
>
<Icon
name="tabler:chevron-right"
class="rtl:rotate-180"
/>
<NuxtLinkLocale
:to="{ name: 'passwordGroupItems', params: { groupId: item.id } }"
>
{{ item.name }}
</NuxtLinkLocale>
</li>
<li class="ml-2">
<UTooltip :text="t('edit')">
<NuxtLinkLocale
:to="{
name: 'passwordGroupEdit',
params: { groupId: lastGroup?.id },
}"
>
<NuxtLinkLocale
:to="{
name: 'passwordGroupEdit',
params: { groupId: lastGroup?.id },
}"
>
<Icon name="mdi:pencil" />
</NuxtLinkLocale>
</UiTooltip>
</li>
</ul>
</div>
<Icon name="mdi:pencil" />
</NuxtLinkLocale>
</UTooltip>
</li>
</ul>
</template>
<script setup lang="ts">
import { UiTooltip } from '#components'
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const groups = defineProps<{ items: SelectHaexPasswordsGroups[] }>()

View File

@ -1,11 +1,10 @@
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
export const usePasswordGroup = () => {
const areItemsEqual = (
groupA: unknown | unknown[] | null,
groupB: unknown | unknown[] | null,
) => {
if (groupA === null && groupB === null) return true
console.log('compare values', groupA, groupB)
if (groupA === groupB) return true
if (Array.isArray(groupA) && Array.isArray(groupB)) {
console.log('compare object arrays', groupA, groupB)
@ -18,7 +17,55 @@ export const usePasswordGroup = () => {
return areObjectsEqual(groupA, groupB)
}
const deepEqual = (obj1: unknown, obj2: unknown) => {
console.log('compare values', obj1, obj2)
if (obj1 === obj2) return true
// Null/undefined Check
if (obj1 == null || obj2 == null) return obj1 === obj2
// Typ-Check
if (typeof obj1 !== typeof obj2) return false
// Primitive Typen
if (typeof obj1 !== 'object') return obj1 === obj2
// Arrays
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) return false
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i])) return false
}
return true
}
// Date Objekte
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
// RegExp Objekte
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// Objekte
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) return false
for (const key of keys1) {
if (!keys2.includes(key)) return false
if (!deepEqual(obj1[key], obj2[key])) return false
}
return true
}
return {
areItemsEqual,
deepEqual,
}
}

View File

@ -1,84 +1,78 @@
<template>
<div class="p-1">
<UiCard
v-if="group"
:title="mode === 'edit' ? t('title.edit') : t('title.create')"
icon="mdi:folder-plus-outline"
@close="$emit('close')"
body-class="px-0"
>
<form
class="flex flex-col gap-4 w-full p-4"
@submit.prevent="$emit('submit')"
>
<UiInput
:label="t('name')"
:placeholder="t('name')"
:read_only
autofocus
v-model="group.name"
ref="nameRef"
@keyup.enter="$emit('submit')"
<UCard v-if="group">
<template #header>
<div class="flex items-center gap-2">
<Icon
:name="
mode === 'edit'
? 'mdi:folder-edit-outline'
: 'mdi:folder-plus-outline'
"
size="24"
/>
<span>{{ mode === 'edit' ? t('title.edit') : t('title.create') }}</span>
</div>
</template>
<UiInput
v-model="group.description"
:label="t('description')"
:placeholder="t('description')"
:read_only
@keyup.enter="$emit('submit')"
/>
<form class="flex flex-col gap-4 w-full p-4">
<UiInput
ref="nameRef"
v-model="group.name"
:label="t('name')"
:placeholder="t('name')"
:read-only
autofocus
@keyup.enter="$emit('submit')"
/>
<div class="flex flex-wrap gap-4">
<UiSelectIcon
<UiInput
v-model="group.description"
:label="t('description')"
:placeholder="t('description')"
:read-only
@keyup.enter="$emit('submit')"
/>
<div class="flex flex-wrap gap-4">
<!-- <UiSelectIcon
v-model="group.icon"
default-icon="mdi:folder-outline"
:read_only
:readOnly
/>
<UiSelectColor
v-model="group.color"
:read_only
/>
</div>
<!-- <div class="flex flex-wrap justify-end gap-4">
<UiButton
class="btn-error btn-outline flex-1"
@click="$emit('close')"
>
{{ t('abort') }}
<Icon name="mdi:close" />
</UiButton>
<UiButton
class="btn-primary flex-1"
@click="$emit('submit')"
>
{{ mode === 'create' ? t('create') : t('save') }}
<Icon name="mdi:check" />
</UiButton>
</div> -->
</form>
</UiCard>
</div>
:readOnly
/> -->
</div>
</form>
</UCard>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const group = defineModel<SelectHaexPasswordsGroups | null>()
const { read_only = false } = defineProps<{
read_only?: boolean
const { readOnly = false } = defineProps<{
readOnly?: boolean
mode: 'create' | 'edit'
}>()
defineEmits(['close', 'submit'])
const emit = defineEmits(['close', 'submit'])
const { t } = useI18n()
const nameRef = useTemplateRef('nameRef')
onStartTyping(() => {
nameRef.value?.inputRef?.focus()
nameRef.value?.$el.focus()
})
const { escape } = useMagicKeys()
watchEffect(async () => {
if (escape?.value) {
await nextTick()
emit('close')
}
})
</script>

View File

@ -5,73 +5,73 @@
@submit.prevent="$emit('submit')"
>
<UiInput
v-show="!read_only || itemDetails.title"
v-show="!readOnly || itemDetails.title"
ref="titleRef"
v-model.trim="itemDetails.title"
:check-input="check"
:label="t('item.title')"
:placeholder="t('item.title')"
:read_only
:read-only
:with-copy-button
autofocus
ref="titleRef"
v-model.trim="itemDetails.title"
@keyup.enter="$emit('submit')"
/>
<UiInput
v-show="!read_only || itemDetails.username"
v-show="!readOnly || itemDetails.username"
v-model.trim="itemDetails.username"
:check-input="check"
:label="t('item.username')"
:placeholder="t('item.username')"
:with-copy-button
:read_only
v-model.trim="itemDetails.username"
:read-only
@keyup.enter="$emit('submit')"
/>
<UiInputPassword
v-show="!read_only || itemDetails.password"
:check-input="check"
:read_only
:with-copy-button
v-show="!readOnly || itemDetails.password"
v-model.trim="itemDetails.password"
:check-input="check"
:read-only
:with-copy-button
@keyup.enter="$emit('submit')"
>
<template #append>
<UiDialogPasswordGenerator
v-if="!read_only"
<!-- <UiDialogPasswordGenerator
v-if="!readOnly"
class="join-item"
:password="itemDetails.password"
v-model="preventClose"
/>
/> -->
</template>
</UiInputPassword>
<UiInputUrl
v-show="!read_only || itemDetails.url"
v-show="!readOnly || itemDetails.url"
v-model="itemDetails.url"
:label="t('item.url')"
:placeholder="t('item.url')"
:read_only
:read-only
:with-copy-button
v-model="itemDetails.url"
@keyup.enter="$emit('submit')"
/>
<UiSelectIcon
v-show="!read_only"
<!-- <UiSelectIcon
v-show="!readOnly"
:default-icon="defaultIcon || 'mdi:key-outline'"
:read_only
:readOnly
v-model="itemDetails.icon"
/>
/> -->
<UiTextarea
v-show="!read_only || itemDetails.note"
v-show="!readOnly || itemDetails.note"
v-model="itemDetails.note"
:label="t('item.note')"
:placeholder="t('item.note')"
:read_only
:readOnly
:with-copy-button
@keyup.enter.stop
class="h-52"
color="error"
/>
</form>
</div>
@ -82,7 +82,7 @@ import type { SelectHaexPasswordsItemDetails } from '~~/src-tauri/database/schem
defineProps<{
defaultIcon?: string | null
read_only?: boolean
readOnly?: boolean
withCopyButton?: boolean
}>()
@ -93,7 +93,7 @@ const itemDetails = defineModel<SelectHaexPasswordsItemDetails>({
required: true,
})
const preventClose = defineModel<boolean>('preventClose')
//const preventClose = defineModel<boolean>('preventClose')
const check = defineModel<boolean>('check-input', { default: false })
@ -104,7 +104,7 @@ const check = defineModel<boolean>('check-input', { default: false })
const titleRef = useTemplateRef('titleRef')
onStartTyping(() => {
titleRef.value?.inputRef?.focus()
titleRef.value?.$el?.focus()
})
</script>

View File

@ -1,70 +1,41 @@
<template>
<div class="p-1">
<UiCard
body-class="rounded overflow-auto p-0 h-full"
<UCard
class="rounded overflow-auto p-0 h-full"
@close="onClose"
>
<div class="">
<nav
aria-label="Tabs Password Item"
aria-orientation="horizontal"
class="tabs tabs-bordered w-full transition-all duration-700 sticky top-0 z-10"
role="tablist"
<UTabs
:items="tabs"
variant="link"
:ui="{ trigger: 'grow' }"
class="gap-4 w-full"
>
<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"
<template #details>
<HaexPassItemDetails
v-if="details"
v-model="details"
with-copy-button
:read-only
:defaultIcon
v-model:prevent-close="preventClose"
@submit="$emit('submit')"
/>
<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>
</template>
<div class="h-full pb-8">
<template #keyValue>
<HaexPassItemKeyValue
v-if="keyValues"
v-model="keyValues"
v-model:items-to-add="keyValuesAdd"
v-model:items-to-delete="keyValuesDelete"
:read-only
:item-id="details!.id"
/>
</template>
</UTabs>
<!-- <div class="h-full pb-8">
<div
id="vaultDetailsId"
role="tabpanel"
@ -104,15 +75,16 @@
role="tabpanel"
:aria-labelledby="id.history"
>
<!-- <HaexPassItemHistory v-model="itemHistory" /> -->
<HaexPassItemHistory />
</div>
</div>
</div> -->
</div>
</UiCard>
</UCard>
</div>
</template>
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
@ -125,13 +97,13 @@ defineProps<{
}>()
const emit = defineEmits<{
close: [void]
addKeyValue: [void]
close: []
addKeyValue: []
removeKeyValue: [string]
submit: [void]
submit: []
}>()
const read_only = defineModel<boolean>('read_only', { default: false })
const readOnly = defineModel<boolean>('readOnly', { default: false })
const details = defineModel<SelectHaexPasswordsItemDetails | null>('details', {
required: true,
@ -152,12 +124,12 @@ const keyValuesDelete = defineModel<SelectHaexPasswordsItemKeyValues[]>(
const { t } = useI18n()
const id = reactive({
/* const id = reactive({
details: useId(),
keyValue: useId(),
history: useId(),
content: {},
})
}) */
const preventClose = ref(false)
@ -166,6 +138,24 @@ const onClose = () => {
emit('close')
}
const tabs = ref<TabsItem[]>([
{
label: t('tab.details'),
icon: 'material-symbols:key-outline',
slot: 'details' as const,
},
{
label: t('tab.keyValue'),
icon: 'fluent:group-list-20-filled',
slot: 'keyValue' as const,
},
{
label: t('tab.history'),
icon: 'material-symbols:history',
slot: 'history' as const,
},
])
</script>
<i18n lang="json">

View File

@ -12,47 +12,44 @@
class="flex gap-2 hover:bg-primary/20 px-4 items-center"
@click="currentSelected = item"
>
<button class="link flex items-center no-underline w-full py-2">
<button class="flex items-center no-underline w-full py-2">
<input
v-model="item.key"
:readonly="currentSelected !== item || read_only"
:readonly="currentSelected !== item || readOnly"
class="flex-1 cursor-pointer"
/>
</button>
<UiButton
v-if="!read_only"
v-if="!readOnly"
:class="[currentSelected === item ? 'visible' : 'invisible']"
class="inline-flex btn-square btn-error btn-outline"
variant="outline"
color="error"
icon="mdi:trash-outline"
@click="deleteItem(item.id)"
>
<Icon
name="mdi:trash-outline"
class="size-5"
/>
</UiButton>
/>
</li>
</UiList>
<UiTextarea
<UTextarea
v-if="items.length || itemsToAdd.length"
:read_only="read_only || !currentSelected"
:readOnly="readOnly || !currentSelected"
class="flex-1 min-w-52 border-base-content/25"
rows="6"
v-model="currentValue"
with-copy-button
/>
</div>
<div
v-show="!read_only"
v-show="!readOnly"
class="flex py-4 gap-2 justify-center items-end flex-wrap"
>
<UiButton
@click="addItem"
class="btn-primary btn-outline flex-1-1 min-w-40"
icon="mdi:plus"
>
<Icon name="mdi:plus" />
<p class="hidden sm:inline-block">{{ t('add') }}</p>
<!-- <Icon name="mdi:plus" />
<p class="hidden sm:inline-block">{{ t('add') }}</p> -->
</UiButton>
</div>
</div>
@ -61,7 +58,7 @@
<script setup lang="ts">
import type { SelectHaexPasswordsItemKeyValues } from '~~/src-tauri/database/schemas/vault'
const { itemId } = defineProps<{ read_only?: boolean; itemId: string }>()
const { itemId } = defineProps<{ readOnly?: boolean; itemId: string }>()
const items = defineModel<SelectHaexPasswordsItemKeyValues[]>({ default: [] })
@ -74,9 +71,9 @@ const itemsToAdd = defineModel<SelectHaexPasswordsItemKeyValues[]>(
{ default: [] },
)
defineEmits<{ add: [void]; remove: [string] }>()
defineEmits<{ add: []; remove: [string] }>()
const { t } = useI18n()
//const { t } = useI18n()
const currentSelected = ref<SelectHaexPasswordsItemKeyValues | undefined>(
items.value?.at(0),
@ -101,6 +98,7 @@ const addItem = () => {
key: '',
value: '',
updateAt: null,
haex_tombstone: null,
})
}

View File

@ -1,70 +1,60 @@
<template>
<div
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end"
:class="[isVisible ? 'left-15 ' : 'left-0']"
class="fixed bottom-4 flex justify-between transition-all pointer-events-none right-0 sm:items-center items-end h-12"
:class="[isVisible ? 'left-16' : 'left-0']"
>
<div class="flex items-center justify-center flex-1">
<UiButton
v-show="showCloseButton"
:tooltip="t('abort')"
icon="mdi:close"
color="error"
variant="ghost"
class="pointer-events-auto"
@click="$emit('close')"
class="btn-accent btn-square"
>
<Icon name="mdi:close" />
</UiButton>
/>
</div>
<div>
<UiButton
v-show="showEditButton"
icon="mdi:pencil-outline"
class="pointer-events-auto"
size="xl"
:tooltip="t('edit')"
@click="$emit('edit')"
class="btn-xl btn-square btn-primary"
>
<Icon
name="mdi:pencil-outline"
class="size-11 shrink-0"
/>
</UiButton>
/>
<UiButton
v-show="showReadonlyButton"
icon="mdi:pencil-off-outline"
class="pointer-events-auto"
size="xl"
:tooltip="t('readonly')"
class="btn-xl btn-square btn-primary"
@click="$emit('readonly')"
>
<Icon
name="mdi:pencil-off-outline"
class="size-11 shrink-0"
/>
</UiButton>
/>
<UiButton
v-show="showSaveButton"
icon="mdi:content-save-outline"
size="xl"
class="pointer-events-auto"
:class="{ 'animate-pulse': hasChanges }"
:tooltip="t('save')"
class="btn-xl btn-square btn-primary motion-duration-2000"
:class="{ 'motion-preset-pulse-sm': hasChanges }"
@click="$emit('save')"
>
<Icon
name="mdi:content-save-outline"
class="size-11 shrink-0"
/>
</UiButton>
/>
</div>
<div class="flex items-center justify-center flex-1">
<UiButton
v-show="showDeleteButton"
color="error"
icon="mdi:trash-outline"
class="pointer-events-auto"
variant="ghost"
:tooltip="t('delete')"
class="btn-square btn-error"
@click="$emit('delete')"
>
<Icon
name="mdi:trash-outline"
class="shrink-0"
/>
</UiButton>
/>
</div>
</div>
</template>
@ -74,12 +64,12 @@ const { isVisible } = storeToRefs(useSidebarStore())
const { t } = useI18n()
defineProps<{
hasChanges?: boolean
showCloseButton?: boolean
showDeleteButton?: boolean
showEditButton?: boolean
showReadonlyButton?: boolean
showSaveButton?: boolean
hasChanges?: boolean
}>()
defineEmits(['close', 'edit', 'readonly', 'save', 'delete'])

View File

@ -4,15 +4,21 @@
class="flex-1"
>
<ul
class="flex flex-col w-full h-full gap-y-2 first:rounded-t-md last:rounded-b-md p-1"
ref="listRef"
class="flex flex-col w-full h-full gap-y-2 first:rounded-t-md last:rounded-b-md p-1"
>
<li
v-for="(item, index) in menuItems"
:key="item.id"
class="bg-base-100 rounded-lg hover:bg-base-content/20 origin-to intersect:motion-preset-slide-down intersect:motion-ease-spring-bouncier intersect:motion-delay ease-in-out shadow"
v-on-long-press="[
onLongPressCallbackHook,
{
delay: 1000,
},
]"
class="bg-accented rounded-lg hover:bg-base-content/20 origin-to intersect:motion-preset-slide-down intersect:motion-ease-spring-bouncier intersect:motion-delay ease-in-out shadow"
:class="{
'bg-base-content/30 outline outline-accent hover:bg-base-content/20':
'bg-elevated/30 outline outline-accent hover:bg-base-content/20':
selectedItems.has(item) ||
(currentSelectedItem?.id === item.id &&
longPressedHook &&
@ -22,12 +28,6 @@
),
}"
:style="{ '--motion-delay': `${50 * index}ms` }"
v-on-long-press="[
onLongPressCallbackHook,
{
delay: 1000,
},
]"
@mousedown="
longPressedHook
? (currentSelectedItem = null)
@ -85,7 +85,7 @@ const { search } = storeToRefs(useSearchStore())
const onClickItemAsync = async (item: IPasswordMenuItem) => {
currentSelectedItem.value = null
if (longPressedHook.value || selectedItems.value.size || ctrl.value) {
if (longPressedHook.value || selectedItems.value.size || ctrl?.value) {
if (selectedItems.value?.has(item)) {
selectedItems.value.delete(item)
} else {

View File

@ -1,188 +0,0 @@
<template>
<aside
:id
ref="sidebarRef"
class="flex sm:shadow-none w-full md:max-w-64"
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,42 +0,0 @@
<script setup lang="ts">
const onClick = () => {
console.log("click")
}
</script>
<template>
<Dialog>
<DialogTrigger as-child>
<Button variant="outline" @click="onClick">
Edit Profile
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="name" class="text-right">
Name
</Label>
<Input id="name" default-value="Pedro Duarte" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="username" class="text-right">
Username
</Label>
<Input id="username" default-value="@peduarte" class="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@ -1,48 +0,0 @@
<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

@ -1,107 +0,0 @@
<template>
<div class="z-10 pointer-events-auto">
<div
class="dropdown relative inline-flex [--placement:top] [--strategy:absolute]"
>
<button
:id
class="dropdown-toggle btn btn-primary btn-xl btn-square dropdown-open:rotate-45 transition-transform"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Menu"
>
<Icon
:name="icon"
class="size-11 shrink-0"
/>
</button>
<ul
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60 bg-transparent shadow-none"
data-dropdown-transition
role="menu"
aria-orientation="vertical"
:aria-labelledby="id"
>
<li
v-for="link in menu"
class="dropdown-item hover:bg-transparent px-0 py-1"
>
<NuxtLinkLocale
v-if="link.to"
:to="link.to"
class="btn btn-primary flex items-center no-underline rounded-lg flex-nowrap w-full"
>
<Icon
v-if="link.icon"
:name="link.icon"
class="me-3"
/>
{{ te(link.label) ? t(link.label) : link.label }}
</NuxtLinkLocale>
<button
v-else
@click="link.action"
class="link hover:link-primary flex items-center no-underline w-full"
>
<Icon
v-if="link.icon"
:name="link.icon"
class="me-3"
/>
{{ link.label }}
</button>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import type { IActionMenuItem } from './types'
defineProps({
menu: {
type: Array as PropType<IActionMenuItem[]>,
},
icon: {
type: String,
default: 'mdi:plus',
},
})
const id = useId()
const { t, te } = useI18n()
</script>
<style lang="css" scoped>
@keyframes fadeInStagger {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 2. Die Listenelemente sind standardmäßig unsichtbar, damit sie nicht aufblitzen */
.stagger-menu li {
opacity: 0;
}
/* 3. Wenn das Menü geöffnet wird, weise die Animation zu */
:global(.dropdown-open) .stagger-menu li {
animation-name: fadeInStagger;
animation-duration: 0.4s;
animation-timing-function: ease-out;
/* SEHR WICHTIG: Sorgt dafür, dass die Elemente nach der Animation sichtbar bleiben (den Zustand von 'to' beibehalten) */
animation-fill-mode: forwards;
/* Die individuelle animation-delay wird per :style im Template gesetzt. */
}
</style>

View File

@ -1,22 +1,31 @@
<template>
<button
class="btn join-item pointer-events-auto"
:type
>
<UiTooltip
:tooltip
v-if="tooltip"
>
<slot />
</UiTooltip>
<slot v-else />
</button>
<div>
<UTooltip :text="buttonProps?.tooltip">
<UButton
class="pointer-events-auto"
v-bind="{ ...buttonProps, ...$attrs }"
@click="(e) => $emit('click', e)"
>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
/>
</template>
</UButton>
</UTooltip>
</div>
</template>
<script setup lang="ts">
const { type = 'button' } = defineProps<{
type?: 'reset' | 'submit' | 'button'
import type { ButtonProps } from '@nuxt/ui'
interface IButtonProps extends /* @vue-ignore */ ButtonProps {
tooltip?: string
}>()
}
const buttonProps = defineProps<IButtonProps>()
defineEmits<{ click: [Event] }>()
</script>

View File

@ -1,8 +0,0 @@
import type { RouteLocationRaw } from 'vue-router'
export interface IActionMenuItem {
label: string
icon?: string
action?: () => Promise<unknown>
to?: RouteLocationRaw
}

View File

@ -1,81 +0,0 @@
<template>
<div class="card min-w-56">
<slot name="image" />
<div
class="card-header"
v-if="$slots.title || title"
>
<slot name="header">
<div
v-if="$slots.title || title"
class="flex items-center gap-2"
>
<Icon
v-if="icon"
:name="icon"
size="28"
/>
<h5
v-if="title"
class="card-title mb-0"
>
{{ title }}
</h5>
<slot
v-else
name="title"
/>
</div>
<div class="text-base-content/45">{{ subtitle }}</div>
</slot>
</div>
<div
class="card-body"
:class="bodyClass"
>
<slot />
<div
v-if="$slots.action"
class="card-actions"
>
<slot name="action" />
</div>
</div>
<div
v-if="$slots.footer"
class="card-footer"
>
<slot name="footer" />
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['close', 'submit'])
defineProps<{
title?: string
subtitle?: string
icon?: string
bodyClass?: string
}>()
const { escape, enter } = useMagicKeys()
watchEffect(async () => {
if (escape.value) {
await nextTick()
emit('close')
}
})
watchEffect(async () => {
if (enter.value) {
await nextTick()
emit('submit')
}
})
</script>

View File

@ -1,61 +1,73 @@
<template>
<UiDialog
:title
@close="onAbort"
<UModal
v-model:open="open"
:title
:description
:fullscreen="isSmallScreen"
>
<template #trigger>
<slot name="trigger" />
</template>
<slot>
<!-- <UiButton
color="primary"
variant="outline"
icon="mdi:menu"
:ui="{
base: '',
}"
/> -->
</slot>
<template #title>
<slot name="title" />
</template>
<slot />
<template #buttons>
<slot name="buttons">
<UiButton
class="btn-error btn-outline w-full sm:w-auto"
@click="onAbort"
>
<Icon :name="abortIcon || 'mdi:close'" />
{{ abortLabel ?? t('abort') }}
</UiButton>
<UiButton
class="btn-primary w-full sm:w-auto"
@click="onConfirm"
>
<Icon :name="confirmIcon || 'mdi:check'" />
{{ confirmLabel ?? t('confirm') }}
</UiButton>
</slot>
<template #body>
<slot name="body" />
</template>
</UiDialog>
<template #footer>
<div class="flex flex-col sm:flex-row gap-4 justify-end w-full">
<UiButton
:icon="abortIcon || 'mdi:close'"
:label="abortLabel || t('abort')"
block
color="error"
variant="outline"
@click="open = false"
/>
<UiButton
:icon="confirmIcon || 'mdi:check'"
:label="confirmLabel || t('confirm')"
block
color="primary"
varaint="solid"
@click="$emit('confirm')"
/>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
defineProps<{
confirmLabel?: string
abortLabel?: string
title?: string
abortIcon?: string
abortLabel?: string
confirmIcon?: string
confirmLabel?: string
description?: string
title?: string
}>()
const open = defineModel<boolean>('open', { default: false })
const { t } = useI18n()
const emit = defineEmits(['confirm', 'abort'])
defineEmits(['confirm'])
const onAbort = () => {
emit('abort')
}
const breakpoints = useBreakpoints(breakpointsTailwind)
const onConfirm = () => {
emit('confirm')
}
// "smAndDown" gilt für sm, xs usw.
const isSmallScreen = breakpoints.smaller('sm')
</script>
<i18n lang="yaml">

View File

@ -1,116 +0,0 @@
<template>
<button
v-if="$slots.trigger || label"
v-bind="$attrs"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
:aria-label="label"
@click="$emit('open')"
>
<slot name="trigger">
{{ label }}
</slot>
</button>
<div class="hidden">
<Teleport to="body">
<div
:id
ref="modalRef"
class="overlay modal overlay-open:opacity-100 overlay-open:duration-300 hidden modal-middle p-0 xs:p-2 --prevent-on-load-init pointer-events-auto max-w-none"
role="dialog"
tabindex="-1"
>
<div
class="overlay-animation-target overlay-open:duration-300 overlay-open:opacity-100 transition-all ease-out modal-dialog"
>
<div class="modal-content justify-between">
<div class="modal-header py-0 sm:py-4">
<div
v-if="title || $slots.title"
class="modal-title py-4 break-all"
>
<slot name="title">
{{ title }}
</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>
</div>
<div class="modal-body text-sm sm:text-base grow mt-0 pt-0">
<slot />
</div>
<div class="modal-footer flex-col sm:flex-row">
<slot name="buttons" />
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import type { HSOverlay } from 'flyonui/flyonui'
const { currentTheme } = storeToRefs(useUiStore())
defineProps<{ title?: string; label?: string }>()
const emit = defineEmits(['open', 'close'])
const id = useId()
const open = defineModel<boolean>('open', { default: false })
const { t } = useI18n()
const modalRef = useTemplateRef('modalRef')
defineExpose({ modalRef })
const modal = ref<HSOverlay>()
watch(open, async () => {
if (!modal.value) return
if (open.value) {
await modal.value.open()
} else {
await modal.value.close(true)
emit('close')
}
})
onMounted(async () => {
if (!modalRef.value) return
modal.value = new window.HSOverlay(modalRef.value)
modal.value.isLayoutAffect = true
modal.value.on('close', () => {
open.value = false
})
})
</script>
<i18n lang="yaml">
de:
close: Schließen
en:
close: Close
</i18n>

View File

@ -1,55 +0,0 @@
<template>
<UiDialogConfirm
:confirm-label="t('apply')"
:title="t('title')"
@abort="open = false"
@click="open = true"
class="btn btn-square btn-accent btn-outline"
v-model:open="open"
>
<template #trigger>
<Icon name="mdi:dice" />
</template>
<form class="flex flex-col gap-4">
<UiInputPassword
v-model="newPassword"
prepend-icon="mdi:key-outline"
with-copy-button
>
<template #append>
<UiButton class="btn-square btn-accent btn-outline">
<Icon name="mdi:refresh" />
</UiButton>
</template>
</UiInputPassword>
</form>
</UiDialogConfirm>
</template>
<script setup lang="ts">
const open = defineModel<boolean>()
const { t } = useI18n()
const { password } = defineProps<{
autofocus?: boolean
checkInput?: boolean
label?: string
placeholder?: string
withCopyButton?: boolean
password: string | null
}>()
const newPassword = computed(() => password)
</script>
<i18n lang="yaml">
de:
title: Passwortgenerator
apply: Übernehmen
en:
title: Passwordgenerator
apply: Apply
</i18n>

View File

@ -1,70 +0,0 @@
<template>
<div
:class="offset"
class="dropdown relative inline-flex"
>
<button
:aria-label="label"
:id
aria-expanded="false"
aria-haspopup="menu"
class="dropdown-toggle"
type="button"
v-bind="$attrs"
>
<slot name="activator">
{{ label }}
<span
class="icon-[tabler--chevron-down] dropdown-open:rotate-180 size-4"
/>
</slot>
</button>
<ul
:aria-labelledby="id"
aria-orientation="vertical"
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-28 z-20 shadow shadow-primary"
role="menu"
>
<slot
name="items"
:items
>
<li
:is="itemIs"
@click="read_only ? '' : $emit('select', item)"
class="dropdown-item"
v-for="item in items"
>
<slot
:item
name="item"
>
{{ item }}
</slot>
</li>
</slot>
</ul>
</div>
</template>
<script setup lang="ts" generic="T">
defineOptions({
inheritAttrs: false,
})
const { itemIs = 'li', offset = '[--offset:0]' } = defineProps<{
label?: string
items?: T[]
itemIs?: string
activatorClass?: string
offset?: string
read_only?: boolean
}>()
defineEmits<{ select: [T] }>()
const id = useId()
//const offset = '[--offset:30]'
</script>

View File

@ -1,40 +1,38 @@
<template>
<UiDropdown
:items="availableLocales"
class="btn btn-primary btn-outline"
@select="(locale) => $emit('select', locale)"
<UDropdownMenu
arrow
:items
:ui="{}"
>
<template #activator>
<Icon :name="flags[locale]" />
<Icon
name="tabler:chevron-down"
class="dropdown-open:rotate-180 size-4"
/>
</template>
<template #item="{ item }">
<div class="flex gap-2 justify-center">
<Icon
:name="flags[item]"
class="my-auto"
/>
<p>
{{ item }}
</p>
</div>
</template>
</UiDropdown>
<UButton
:icon="items.find((item) => item.label === locale)?.icon"
:label="locale"
color="neutral"
variant="outline"
/>
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
import type { Locale } from 'vue-i18n'
const { locales, locale } = useI18n()
const flags = {
de: 'emojione:flag-for-germany',
en: 'emojione:flag-for-united-kingdom',
de: 'circle-flags:de',
en: 'circle-flags:uk',
}
const { availableLocales, locale } = useI18n()
const emit = defineEmits<{ select: [Locale] }>()
defineEmits<{ select: [Locale] }>()
const items = computed<DropdownMenuItem[]>(() =>
locales.value.map((locale) => ({
label: locale.code,
icon: flags[locale.code],
onSelect() {
emit('select', locale.code)
},
})),
)
</script>

View File

@ -1,22 +1,24 @@
<template>
<UiDropdown :items="availableThemes" class="btn btn-primary btn-outline" @select="(theme) => $emit('select', theme)">
<template #activator>
<Icon :name="currentTheme.icon" />
</template>
<template #item="{ item }">
<div class="flex gap-2 justify-center">
<Icon :name="item.icon" class="my-auto" />
<p>
{{ item.name }}
</p>
</div>
</template>
</UiDropdown>
<UDropdownMenu :items>
<UButton :icon="currentTheme?.icon" />
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const { availableThemes, currentTheme } = storeToRefs(useUiStore())
defineEmits<{ select: [ITheme] }>()
</script>
const emit = defineEmits<{ select: [string] }>()
watchImmediate(availableThemes, () =>
console.log('availableThemes', availableThemes),
)
const items = computed<DropdownMenuItem[]>(() =>
availableThemes?.value.map((theme) => ({
...theme,
onSelect: () => emit('select', theme.value),
})),
)
</script>

View File

@ -0,0 +1,45 @@
<template>
<UDropdownMenu :items>
<UButton
icon="mdi:menu"
color="neutral"
variant="outline"
/>
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const { t } = useI18n()
const { closeAsync } = useVaultStore()
const onVaultCloseAsync = async () => {
await closeAsync()
await navigateTo(useLocalePath()({ name: 'vaultOpen' }))
}
const items: DropdownMenuItem[] = [
{
icon: 'tabler:settings',
label: t('settings'),
to: useLocalePath()({ name: 'settings' }),
},
{
icon: 'tabler:logout',
label: t('close'),
onSelect: () => onVaultCloseAsync(),
color: 'error',
},
]
</script>
<i18n lang="yaml">
de:
settings: 'Einstellungen'
close: 'Vault schließen'
en:
settings: 'Settings'
close: 'Close Vault'
</i18n>

View File

@ -1,181 +1,88 @@
<template>
<div>
<fieldset
class="join w-full"
:class="{ 'pt-1.5': label }"
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
:autofocus
:id
:name="name ?? id"
:placeholder="placeholder || label"
:readonly="read_only"
:type
class="ps-2"
ref="inputRef"
v-model="input"
@keyup="(e:KeyboardEvent) => $emit('keyup', e)"
/>
<label
:for="id"
class="input-floating-label"
>
{{ label }}
</label>
</div>
<Icon
v-if="appendIcon"
:name="appendIcon"
class="my-auto shrink-0"
/>
</div>
<UiButton
v-if="withClearButton"
class="btn-outline btn-square"
@click="input = ''"
>
<Icon name="mdi:close" />
</UiButton>
<slot name="append" />
<UiButton
v-if="withCopyButton"
:tooltip="t('copy')"
class="btn-outline btn-accent btn-square"
@click="copy(`${input}`)"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
</fieldset>
<span
v-show="errors"
class="flex flex-col px-2 pt-0.5"
<UInput
v-model="value"
:placeholder="props.placeholder || ' '"
:readonly="props.readOnly"
:leading-icon="props.leadingIcon"
:ui="{ base: 'peer' }"
@change="(e) => $emit('change', e)"
@blur="(e) => $emit('blur', e)"
@keyup="(e: KeyboardEvent) => $emit('keyup', e)"
@keydown="(e: KeyboardEvent) => $emit('keydown', e)"
>
<label
class="absolute pointer-events-none -top-2.5 left-0 text-highlighted text-xs font-medium px-1.5 transition-all peer-focus:-top-2.5 peer-focus:text-highlighted peer-focus:text-xs peer-focus:font-medium peer-placeholder-shown:text-sm peer-placeholder-shown:text-dimmed peer-placeholder-shown:top-1.5 peer-placeholder-shown:font-normal"
>
<span
v-for="error in errors"
class="label-text-alt text-error"
class="inline-flex bg-default px-1"
:class="props?.leadingIcon ? 'mx-6' : 'mx-0'"
>
{{ error }}
{{ props?.label }}
</span>
</span>
</div>
</label>
<template #trailing>
<slot name="trailing" />
<UiButton
v-show="props.withCopyButton"
:color="copied ? 'success' : 'neutral'"
:tooltip="t('copy')"
:icon="copied ? 'mdi:check' : 'mdi:content-copy'"
size="sm"
variant="link"
@click="copy(`${value}`)"
/>
</template>
<template
v-for="(_, slotName) in filteredSlots"
#[slotName]="slotProps"
>
<slot
:name="slotName"
v-bind="slotProps"
/>
</template>
</UInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod'
import type { AcceptableValue, InputProps } from '@nuxt/ui'
const input = defineModel<string | number | undefined | null>({
required: true,
})
const value = defineModel<AcceptableValue | undefined>()
const inputRef = useTemplateRef('inputRef')
defineExpose({ inputRef })
const emit = defineEmits<{
error: [string[]]
keyup: [KeyboardEvent]
}>()
const props = defineProps({
placeholder: {
type: String,
default: '',
},
type: {
type: String as PropType<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
>,
default: 'text',
},
label: String,
name: String,
prependIcon: {
type: String,
default: '',
},
prependLabel: String,
appendIcon: {
type: String,
default: '',
},
appendLabel: String,
rules: Object as PropType<ZodSchema>,
checkInput: Boolean,
withCopyButton: Boolean,
withClearButton: Boolean,
autofocus: Boolean,
read_only: Boolean,
})
onMounted(() => {
if (props.autofocus && inputRef.value) inputRef.value.focus()
})
const errors = defineModel<string[] | undefined>('errors')
const id = useId()
watch(input, () => checkInput())
watch(
() => props.checkInput,
() => {
checkInput()
},
)
const checkInput = () => {
if (props.rules) {
const result = props.rules.safeParse(input.value)
//console.log('check result', result.error, props.rules);
if (!result.success) {
errors.value = result.error.errors.map((error) => error.message)
emit('error', errors.value)
} else {
errors.value = []
}
}
interface IInputProps extends /* @vue-ignore */ InputProps {
tooltip?: string
}
const props = defineProps<
IInputProps & {
withCopyButton?: boolean
readOnly?: boolean
label?: string
leadingIcon?: string
}
>()
defineEmits<{
change: [Event]
blur: [Event]
keyup: [KeyboardEvent]
keydown: [KeyboardEvent]
}>()
const { copy, copied } = useClipboard()
const { t } = useI18n()
const filteredSlots = computed(() => {
return Object.fromEntries(
Object.entries(useSlots()).filter(([name]) => name !== 'trailing'),
)
})
watchImmediate(props, () => console.log('props', props))
</script>
<i18n lang="yaml">

View File

@ -1,62 +1,53 @@
<template>
<UiInput
v-model="value"
:autofocus
:check-input
:label="label || t('password')"
:placeholder="placeholder || t('password')"
:rules
:type="type"
:label="t('label')"
:leading-icon
:placeholder="placeholder || ' '"
:read-only
:type="show ? 'text' : 'password'"
:with-copy-button
@keyup="(e) => $emit('keyup', e)"
>
<template #append>
<slot name="append" />
<template #trailing>
<UiButton
class="btn-outline btn-accent btn-square join-item"
@click="tooglePasswordType"
>
<Icon :name="type === 'password' ? 'mdi:eye-off' : 'mdi:eye'" />
</UiButton>
aria-controls="password"
color="neutral"
variant="link"
:aria-label="show ? t('hide') : t('show')"
:aria-pressed="show"
:icon="show ? 'i-lucide-eye-off' : 'i-lucide-eye'"
:tooltip="show ? t('hide') : t('show')"
size="sm"
@click="show = !show"
/>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod'
const { t } = useI18n()
const value = defineModel<string | number | null | undefined>()
import type { AcceptableValue } from '@nuxt/ui'
defineProps<{
autofocus?: boolean
checkInput?: boolean
label?: string
placeholder?: string
rules?: ZodSchema
leadingIcon?: string
withCopyButton?: boolean
readOnly?: boolean
}>()
const value = defineModel<AcceptableValue | undefined>()
defineEmits<{
keyup: [KeyboardEvent]
}>()
const type = ref<'password' | 'text'>('password')
const tooglePasswordType = () => {
type.value = type.value === 'password' ? 'text' : 'password'
}
const show = ref(false)
const { t } = useI18n()
</script>
<i18n lang="json">
{
"de": {
"password": "Passwort"
},
"en": {
"password": "Password"
}
}
<i18n lang="yaml">
de:
show: Passwort ansehen
hide: Passwort verstecken
label: Passwort
en:
show: Show password
hide: Hide password
label: Password
</i18n>

View File

@ -1,5 +1,6 @@
<template>
<UiInput
v-model.trim="value"
:autofocus
:check-input="checkInput"
:label="label || t('url')"
@ -7,17 +8,18 @@
:read_only
:rules
:with-copy-button
v-model.trim="value"
@keyup="(e) => $emit('keyup', e)"
>
<template #append>
<template #trailing>
<UiButton
color="neutral"
variant="link"
size="sm"
icon="streamline:web"
:disabled="!value?.length"
:tooltip="t('browse')"
@click="openUrl(`${value}`)"
class="btn-outline btn-accent btn-square"
>
<Icon name="streamline:web" />
</UiButton>
/>
</template>
</UiInput>
</template>
@ -45,13 +47,12 @@ defineEmits<{
}>()
</script>
<i18n lang="json">
{
"de": {
"url": "Url"
},
"en": {
"url": "Url"
}
}
<i18n lang="yaml">
de:
url: Url
browse: Url öffnen
en:
url: Url
browse: Open url
</i18n>

View File

@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col h-fit border border-base-content/25 divide-base-content/25 divide-y rounded-md first:rounded-t-md last:rounded-b-md"
class="flex flex-col h-fit border border-default/25 divide-default/25 divide-y rounded-md first:rounded-t-md last:rounded-b-md"
>
<slot />
</div>

View File

@ -2,7 +2,6 @@
<svg
viewBox="122 107 263 292"
xmlns="http://www.w3.org/2000/svg"
style="max-height: 500px"
>
<g
stroke-width="0.3"

View File

@ -10,24 +10,23 @@
<input
:id
:readonly="read_only"
:disabled="read_only"
ref="colorRef"
v-model="model"
:readonly="readOnly"
:disabled="readOnly"
:title="t('pick')"
class="top-0 left-0 absolute size-0"
type="color"
v-model="model"
ref="colorRef"
/>
<UiTooltip :tooltip="t('reset')">
<button
<UiButton
color="error"
:class="{ 'btn-disabled': readOnly }"
icon="mdi:refresh"
:disabled="readOnly"
@click="model = ''"
class="btn btn-sm text-sm btn-outline btn-error"
:class="{ 'btn-disabled': read_only }"
type="button"
>
<Icon name="mdi:refresh" />
</button>
/>
</UiTooltip>
</div>
</template>
@ -39,12 +38,12 @@ const { t } = useI18n()
const model = defineModel<string | null>()
const colorRef = useTemplateRef('colorRef')
defineProps({
read_only: Boolean,
readOnly: Boolean,
})
const { currentTheme } = storeToRefs(useUiStore())
const textColorClass = computed(() => {
if (!model.value)
if (!model.value && currentTheme.value)
return currentTheme.value.value === 'dark' ? 'text-black' : 'text-white'
const color = getContrastingTextColor(model.value)

View File

@ -1,5 +1,5 @@
<template>
<UiDropdown
<UDropdownMenu
:items="icons"
class="btn"
@select="(newIcon) => (iconName = newIcon)"
@ -23,7 +23,7 @@
</li>
</div>
</template>
</UiDropdown>
</UDropdownMenu>
</template>
<script setup lang="ts">

View File

@ -0,0 +1,60 @@
<template>
<li
class="rounded hover:bg-elevated py-2 cursor-pointer"
:class="{
['bg-base-content/20 ']: isActive,
}"
@click="triggerNavigate"
>
<UTooltip :tooltip="tooltip ?? name">
<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>
</UTooltip>
</li>
</template>
<script setup lang="ts">
import type { ISidebarItem } from '#imports'
const props = defineProps<ISidebarItem>()
const router = useRouter()
console.log('to', props.to)
const isActive = computed(() => {
if (props.to?.name === 'haexExtension') {
return (
getSingleRouteParam(router.currentRoute.value.params.extensionId) ===
props.id
)
} else {
return (
props.to?.name === router.currentRoute.value.meta.name ||
router
.getRoutes()
.find((route) => route.meta.name === props.to?.name)
?.children.some(
(route) => route.meta?.name === router.currentRoute.value.meta.name,
)
)
}
})
const linkRef = useTemplateRef('linkRef')
const triggerNavigate = () => linkRef.value?.$el.click()
</script>

View File

@ -1,7 +0,0 @@
<template>
<input v-model="value" />
</template>
<script setup lang="ts">
const value = defineModel()
</script>

View File

@ -1,6 +1,6 @@
<template>
<p
class="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent font-black"
class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent font-black"
>
<slot />
</p>

View File

@ -1,41 +1,54 @@
<template>
<div class="relative">
<UiButton
v-if="withCopyButton"
:tooltip="t('copy')"
class="btn-square btn-outline btn-accent absolute z-10 top-2 right-2"
@click="copy(`${value}`)"
<div>
<UTextarea
:id
v-model="value"
:ui="{ base: 'peer' }"
:readonly="readOnly"
class="w-full"
v-bind="$attrs"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
<div class="textarea-floating">
<textarea
:class="{ 'pr-10': withCopyButton }"
:id
:placeholder
:readonly="read_only"
class="textarea"
v-bind="$attrs"
v-model="value"
></textarea>
<label
class="textarea-floating-label"
:for="id"
class="absolute pointer-events-none -top-2.5 left-0 text-highlighted text-xs font-medium px-1.5 transition-all peer-focus:-top-2.5 peer-focus:text-highlighted peer-focus:text-xs peer-focus:font-medium peer-placeholder-shown:text-sm peer-placeholder-shown:text-dimmed peer-placeholder-shown:top-1.5 peer-placeholder-shown:font-normal"
>
{{ label }}
<span class="inline-flex bg-default px-1">
{{ props.label }}
</span>
</label>
</div>
<template #trailing>
<UiButton
v-show="withCopyButton"
:color="copied ? 'success' : 'neutral'"
:tooltip="t('copy')"
:icon="copied ? 'mdi:check' : 'mdi:content-copy'"
size="sm"
variant="link"
@click="copy(`${value}`)"
/>
</template>
</UTextarea>
</div>
</template>
<script setup lang="ts">
defineProps<{
import type { TextareaProps } from '@nuxt/ui'
interface ITextareaProps extends /* @vue-ignore */ TextareaProps {
tooltip?: string
withCopyButton?: boolean
readOnly?: boolean
label?: string
}
const props = defineProps<ITextareaProps>()
/* defineProps<{
placeholder?: string
label?: string
read_only?: boolean
readOnly?: boolean
withCopyButton?: boolean
}>()
}>() */
const id = useId()

View File

@ -1,57 +0,0 @@
<template>
<div class="tooltip [--prevent-popper:false]">
<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 pointer-events-none z-50"
role="tooltip"
>
<span class="tooltip-body">
{{ tooltip }}
</span>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
const props = defineProps({
direction: {
type: String as PropType<
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end'
>,
default: 'top',
},
tooltip: {
type: String,
default: '',
},
trigger: {
type: String as PropType<'focus' | 'hover' | 'click'>,
default: 'hover',
},
})
</script>

View File

@ -1,26 +0,0 @@
<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

@ -1,204 +0,0 @@
<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

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

View File

@ -1,19 +0,0 @@
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,127 +1,101 @@
<template>
<UiDialogConfirm
v-model:open="open"
class="btn btn-primary btn-outline shadow-md btn-lg"
@click="open = true"
@abort="open = false"
@confirm="onCreateAsync"
:confirm-label="t('create')"
@confirm="onCreateAsync"
>
<template #trigger>
<Icon name="mdi:plus" />
{{ t('database.create') }}
</template>
<UiButton
:label="t('vault.create')"
:ui="{
base: 'px-3 py-2',
}"
icon="mdi:plus"
size="xl"
variant="outline"
block
/>
<template #title>
<div class="flex gap-x-2 items-center">
<Icon
name="mdi:safe"
class="text-primary"
/>
<p>
{{ t('title') }}
</p>
</div>
<i18n-t
keypath="title"
tag="p"
class="flex gap-x-2 flex-wrap"
>
<template #haexvault>
<UiTextGradient>HaexVault</UiTextGradient>
</template>
</i18n-t>
</template>
<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
v-model="database.password"
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
/>
</form>
<!-- <template #buttons>
<UiButton
class="btn-error btn-outline w-full sm:w-auto"
@click="onClose"
<template #body>
<UForm
:state="vault"
class="flex flex-col gap-4 w-full h-full justify-center"
>
<Icon name="mdi:x" />
{{ t('abort') }}
</UiButton>
<UiInput
v-model="vault.name"
leading-icon="mdi:safe"
:label="t('vault.label')"
:placeholder="t('vault.placeholder')"
/>
<UiInputPassword
v-model="vault.password"
leading-icon="mdi:key-outline"
/>
<UiButton
class="btn-primary w-full sm:w-auto"
@click="onCreateAsync"
>
{{ t('create') }}
</UiButton>
</template> -->
<UButton
hidden
type="submit"
@click="onCreateAsync"
/>
</UForm>
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import { save } from '@tauri-apps/plugin-dialog'
import { onKeyStroke } from '@vueuse/core'
import { useVaultStore } from '~/stores/vault'
import { vaultDatabaseSchema } from './schema'
import { BaseDirectory, readFile, writeFile } from '@tauri-apps/plugin-fs'
import { resolveResource } from '@tauri-apps/api/path'
//import { convertFileSrc } from "@tauri-apps/api/tauri";
onKeyStroke('Enter', (e) => {
e.preventDefault()
onCreateAsync()
})
const check = ref(false)
const open = ref()
import { vaultSchema } from './schema'
//import type { FormSubmitEvent } from '@nuxt/ui'
const { t } = useI18n()
const database = reactive<{
//type Schema = z.output<typeof vaultSchema>
const vault = reactive<{
name: string
password: string
path: string | null
type: 'password' | 'text'
}>({
name: '',
name: 'HaexVault',
password: '',
path: '',
type: 'password',
})
const initDatabase = () => {
database.name = t('database.name')
database.password = ''
database.path = ''
database.type = 'password'
const initVault = () => {
vault.name = 'HaexVault'
vault.password = ''
vault.path = ''
vault.type = 'password'
}
initDatabase()
const { add } = useSnackbar()
const { createAsync } = useVaultStore()
const { add } = useToast()
const check = ref(false)
const open = ref()
const onCreateAsync = async () => {
check.value = true
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name)
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password,
)
const nameCheck = vaultSchema.name.safeParse(vault.name)
const passwordCheck = vaultSchema.password.safeParse(vault.password)
console.log(
'checks',
database.name,
nameCheck,
database.password,
passwordCheck,
)
console.log('checks', vault.name, nameCheck, vault.password, passwordCheck)
if (!nameCheck.success || !passwordCheck.success) return
open.value = false
@ -130,28 +104,27 @@ const onCreateAsync = async () => {
const template_vault = await readFile(template_vault_path)
database.path = await save({
defaultPath: database.name.endsWith('.db')
? database.name
: `${database.name}.db`,
vault.path = await save({
defaultPath: vault.name.endsWith('.db') ? vault.name : `${vault.name}.db`,
})
if (!database.path) return
if (!vault.path) return
await writeFile('temp_vault.db', template_vault, {
baseDir: BaseDirectory.AppLocalData,
})
console.log('data', database)
console.log('data', vault)
if (database.path && database.password) {
if (vault.path && vault.password) {
const vaultId = await createAsync({
path: database.path,
password: database.password,
path: vault.path,
password: vault.password,
})
console.log('vaultId', vaultId)
if (vaultId) {
initVault()
await navigateTo(
useLocaleRoute()({ name: 'vaultOverview', params: { vaultId } }),
)
@ -159,41 +132,27 @@ const onCreateAsync = async () => {
}
} catch (error) {
console.error(error)
add({ type: 'error', text: `${error}` })
add({ color: 'error', description: `${error}` })
}
}
const onClose = () => {
open.value = false
initDatabase()
}
</script>
<i18n lang="json">
{
"de": {
"database": {
"label": "Vaultname",
"placeholder": "Vaultname",
"create": "Neue Vault anlegen",
"name": "HaexVault"
},
"title": "Neue Vault anlegen",
"create": "Erstellen",
"abort": "Abbrechen",
"description": "Haex Vault für deine geheimsten Geheimnisse"
},
"en": {
"database": {
"label": "Vaultname",
"placeholder": "Vaultname",
"create": "Create new Vault",
"name": "HaexVault"
},
"title": "Create New Vault",
"create": "Create",
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}
<i18n lang="yaml">
de:
vault:
create: Neue Vault erstellen
label: Vaultname
placeholder: Vaultname
name: HaexVault
title: Neue {haexvault} erstellen
create: Erstellen
en:
vault:
create: Create new vault
label: Vaultname
placeholder: Vaultname
name: HaexVault
title: Create new {haexvault}
create: Create
</i18n>

View File

@ -1,67 +1,62 @@
<template>
<UiDialogConfirm
v-model:open="open"
class="btn btn-primary btn-outline shadow-md btn-lg"
:confirm-label="t('open')"
:abort-label="t('abort')"
@abort="onAbort"
:description="vault.path || path"
@confirm="onOpenDatabase"
@open="onLoadDatabase"
>
<UiButton
:label="t('vault.open')"
:ui="{
base: 'px-3 py-2',
}"
icon="mdi:folder-open-outline"
size="xl"
variant="outline"
block
@click.stop="onLoadDatabase"
/>
<template #title>
<i18n-t
keypath="title"
tag="p"
class="flex gap-x-2 flex-wrap"
class="flex gap-x-2 text-wrap"
>
<template #haexvault>
<UiTextGradient>HaexVault</UiTextGradient>
</template>
</i18n-t>
Path1: {{ test }}
<div class="text-sm">{{ props.path ?? database.path }}</div>
</template>
<template #trigger>
<Icon name="mdi:folder-open-outline" />
{{ t('database.open') }}
</template>
<template #body>
<UForm
:state="vault"
class="flex flex-col gap-4 w-full h-full justify-center"
>
<UiInputPassword
v-model="vault.password"
class="w-full"
autofocus
/>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
@keyup.enter="onOpenDatabase"
autofocus
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
<UButton
hidden
type="submit"
@click="onOpenDatabase"
/>
</UForm>
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import { open as openVault } from '@tauri-apps/plugin-dialog'
import { vaultDatabaseSchema } from './schema'
import {
BaseDirectory,
copyFile,
mkdir,
exists,
readFile,
writeFile,
stat,
} from '@tauri-apps/plugin-fs'
import { vaultSchema } from './schema'
const { t } = useI18n()
const open = defineModel('open', { type: Boolean })
const props = defineProps<{
path: string
}>()
const check = ref(false)
const database = reactive<{
const vault = reactive<{
name: string
password: string
path: string | null
@ -73,28 +68,13 @@ const database = reactive<{
type: 'password',
})
const initDatabase = () => {
database.name = ''
database.password = ''
database.path = ''
database.type = 'password'
}
const open = defineModel('open', { type: Boolean })
initDatabase()
const { add } = useSnackbar()
const handleError = (error: unknown) => {
open.value = false
console.error('handleError', error, typeof error)
add({ type: 'error', text: `${error}` })
}
const { openAsync } = useVaultStore()
const { add } = useToast()
const onLoadDatabase = async () => {
try {
database.path = await openVault({
vault.path = await openVault({
multiple: false,
directory: false,
filters: [
@ -105,50 +85,62 @@ const onLoadDatabase = async () => {
],
})
console.log('onLoadDatabase', database.path)
if (!database.path) return
console.log('onLoadDatabase', vault.path)
if (!vault.path) {
open.value = false
return
}
open.value = true
} catch (error) {
handleError(error)
open.value = false
console.error('handleError', error, typeof error)
add({ color: 'error', description: `${error}` })
}
}
const localePath = useLocalePath()
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
useVaultSettingsStore()
const props = defineProps<{
path: string
}>()
const check = ref(false)
const initVault = () => {
vault.name = ''
vault.password = ''
vault.path = ''
vault.type = 'password'
}
const onAbort = () => {
initVault()
open.value = false
}
const onOpenDatabase = async () => {
try {
const { openAsync } = useVaultStore()
const localePath = useLocalePath()
check.value = true
const path = database.path || props.path
const pathCheck = vaultDatabaseSchema.path.safeParse(path)
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password,
)
const path = vault.path || props.path
const pathCheck = vaultSchema.path.safeParse(path)
const passwordCheck = vaultSchema.password.safeParse(vault.password)
/* if (!pathCheck.success || !passwordCheck.success || !path) {
add({
type: 'error',
text: `Params falsch. Path: ${pathCheck.error} | Password: ${passwordCheck.error}`,
})
return
} */
//const vaultName = await copyDatabaseInAppDirectoryAsync(path)
//if (!vaultName) return
if (pathCheck.error || passwordCheck.error) return
const vaultId = await openAsync({
path,
password: database.password,
password: vault.password,
})
if (!vaultId) {
add({
type: 'error',
text: 'Vault konnte nicht geöffnet werden. \n Vermutlich ist das Passwort falsch',
color: 'error',
description: t('error.open'),
})
return
}
@ -169,53 +161,31 @@ const onOpenDatabase = async () => {
syncVaultNameAsync(),
])
} catch (error) {
handleError(error)
open.value = false
console.error('handleError', error, typeof error)
add({ color: 'error', description: `${error}` })
}
}
const test = ref()
const copyDatabaseInAppDirectoryAsync = async (source: string) => {
const vaultName = getFileName(source)
const vaultsDirExists = await exists('vaults', {
baseDir: BaseDirectory.AppLocalData,
})
//convertFileSrc
if (!vaultsDirExists) {
await mkdir('vaults', {
baseDir: BaseDirectory.AppLocalData,
})
}
test.value = `source: ${source} - target: vaults/${vaultName}`
console.log('copyDatabaseInAppDirectoryAsync', `vaults/${vaultName}`)
const sourceFile = await readFile(source)
await writeFile(`vaults/HaexVault.db`, sourceFile, {
baseDir: BaseDirectory.AppLocalData,
})
/* await copyFile(source, `vaults/HaexVault.db`, {
toPathBaseDir: BaseDirectory.AppLocalData,
}) */
return vaultName
}
const onAbort = () => {
initDatabase()
open.value = false
}
</script>
<i18n lang="yaml">
de:
open: Öffnen
abort: Abbrechen
open: Entsperren
title: '{haexvault} entsperren'
database:
password: Passwort
vault:
open: Vault öffnen
description: Öffne eine vorhandene Vault
error:
open: Vault konnte nicht geöffnet werden. \n Vermutlich ist das Passwort falsch
en:
open: Open
abort: Abort
open: Unlock
title: Unlock {haexvault}
database:
password: Passwort
description: Open your existing vault
vault:
open: Open Vault
error:
open: Vault couldn't be opened. \n The password is probably wrong
</i18n>

View File

@ -1,7 +1,7 @@
import { z } from 'zod';
import { z } from 'zod'
export const vaultDatabaseSchema = {
export const vaultSchema = {
password: z.string().min(6).max(255),
name: z.string().min(1).max(255),
path: z.string().min(4).endsWith('.db'),
};
}

View File

@ -1,244 +0,0 @@
<template>
<VaultCard
@close="onClose"
@submit="onSubmit"
>
<template #header>
<div class="flex flex-wrap items-center justify-between w-full px-2 py-3">
<div class="w-full flex gap-2 justify-between items-center">
<div class="flex items-center gap-2">
<button
class="btn btn-square btn-primary btn-outline"
@click="onBack"
>
<Icon
name="mdi:chevron-left"
size="32"
/>
</button>
<button
class="btn btn-square btn-error btn-outline"
@click="onBack"
>
<Icon
name="mdi:trash-can-outline"
size="28"
/>
</button>
</div>
<slot name="buttons">
<div
v-if="read_only"
class="h-full"
>
<button
class="btn btn-square btn-primary btn-outline"
@click="read_only = false"
>
<Icon
name="mdi:pencil-outline"
size="24"
/>
</button>
</div>
<div
v-else
class="gap-2 h-full hidden md:flex"
>
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
>
<Icon name="mdi:close" />
<span class="hidden"> {{ t('abort') }} </span>
</button>
<button
class="btn btn-square btn-success btn-outline"
@click="onSubmit"
>
<Icon name="mdi:check" />
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
</slot>
</div>
<div
class="flex items-center w-full min-h-14 gap-2 py-1"
:style="{ color }"
>
<Icon
v-if="icon"
:name="icon"
size="28"
/>
<h5
v-show="read_only"
class="overflow-hidden whitespace-nowrap"
>
a{{ title }}
</h5>
</div>
</div>
</template>
<div class="h-full">
<slot />
<div
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">
aa
<button
class="btn btn-square btn-error btn-outline"
@click="onClose"
>
<Icon name="mdi:close" />
<span class="hidden"> {{ t('abort') }} </span>
</button>
</div>
<div>
<button
class="btn btn-square btn-success"
@click="onSubmit"
>
<Icon name="mdi:check" />
<span class="hidden"> {{ t('create') }} </span>
</button>
</div>
<div />
</div>
<!-- <UiButtonAction
class=""
icon="mdi:content-save-outline"
><Icon name="mdi:content-save-outline" />
</UiButtonAction> -->
</div>
</VaultCard>
<VaultModalSaveChanges
v-model="showConfirmation"
@reject="onReject"
@submit="onSubmit"
/>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
const { t } = useI18n()
const read_only = defineModel<boolean>('read_only', { default: false })
const props = defineProps<{
color: string
hasChanges: boolean
icon: string
title: string
}>()
const emit = defineEmits<{
back: [void]
close: [void]
reject: [to?: RouteLocationNormalizedLoadedGeneric]
submit: [to?: RouteLocationNormalizedLoadedGeneric]
}>()
const showConfirmation = ref(false)
const to = ref<RouteLocationNormalizedLoadedGeneric>()
const isApprovedForLeave = ref(false)
const wantToGoBack = ref(false)
const onSubmit = () => {
showConfirmation.value = false
isApprovedForLeave.value = true
if (wantToGoBack.value) {
wantToGoBack.value = false
read_only.value = true
emit('submit')
} else {
emit('submit', to.value)
}
}
const onReject = () => {
showConfirmation.value = false
isApprovedForLeave.value = true
read_only.value = true
if (wantToGoBack.value) {
wantToGoBack.value = false
emit('back')
} else emit('reject', to.value)
}
const onBack = () => {
if (props.hasChanges) {
wantToGoBack.value = true
showConfirmation.value = true
} else {
emit('back')
}
}
const onClose = () => {
if (props.hasChanges) {
showConfirmation.value = true
} else {
emit('close') //read_only.value = true;
}
}
onBeforeRouteLeave((_to, _from, next) => {
//console.log('check before leave', _to, _from);
to.value = _to
if (isApprovedForLeave.value) {
isApprovedForLeave.value = false
next()
} else if (props.hasChanges) {
showConfirmation.value = true
} else {
next()
}
})
</script>
<i18n lang="json">
{
"de": {
"create": "Anlegen",
"abort": "Abbrechen",
"entry": {
"title": "Titel",
"username": "Nutzername",
"password": "Passwort",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "Verlauf"
}
},
"en": {
"create": "Create",
"abort": "Abort",
"entry": {
"title": "Title",
"username": "Username",
"password": "Password",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "History"
}
}
}
</i18n>

View File

@ -1,63 +0,0 @@
<template>
<div class="card">
<slot name="image" />
<div class="card-header">
<slot name="header">
<div
v-if="$slots.title || title"
class="flex items-center gap-2"
>
<Icon
v-if="icon"
:name="icon"
size="28"
/>
<h5
v-if="title"
class="card-title mb-0"
>
{{ title }}
</h5>
<slot
v-else
name="title"
/>
</div>
<div class="text-base-content/50">{{ subtitle }}</div>
</slot>
</div>
<div class="card-body px-2 sm:px-6">
<slot />
<div
v-if="$slots.action"
class="card-actions"
>
<slot name="action" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['close', 'submit'])
defineProps<{ title?: string; subtitle?: string; icon?: string }>()
const { escape, enter } = useMagicKeys()
watchEffect(async () => {
if (escape.value) {
await nextTick()
emit('close')
}
})
watchEffect(async () => {
if (enter.value) {
await nextTick()
emit('submit')
}
})
</script>

View File

@ -1 +0,0 @@
<template><div>first time</div></template>

View File

@ -1,32 +0,0 @@
<template>
<UiDialog :title="t('title')">
<form class="flex flex-col">
<UiInput v-model="vaultItem.title" />
<UiInput v-model="vaultItem.username" />
<UiInput v-model="vaultItem.password" />
<UiInput v-model="vaultItem.note" />
</form>
</UiDialog>
</template>
<script setup lang="ts">
const { t } = useI18n();
const vaultItem = reactive({
title: '',
username: '',
password: '',
note: '',
});
</script>
<i18n lang="json">
{
"de": {
"title": "Eintrag erstellen"
},
"en": {
"title": "Create Entry"
}
}
</i18n>

View File

@ -1,112 +0,0 @@
<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'"
:title="vaultGroup.name ?? ''"
@back="$emit('back')"
@close="$emit('close')"
@reject="(to) => $emit('reject', to)"
@submit="(to) => $emit('submit', to)"
>
<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')"
:with-copy-button="read_only"
:read_only
autofocus
/>
<UiInput
v-show="!read_only || vaultGroup.description?.length"
v-model.trim="vaultGroup.description"
:read_only
:label="t('vaultGroup.description')"
:placeholder="t('vaultGroup.description')"
:with-copy-button="read_only"
/>
<UiSelectColor
v-model="vaultGroup.color"
:read_only
:label="t('vaultGroup.color')"
:placeholder="t('vaultGroup.color')"
/>
<UiSelectIcon
v-model="vaultGroup.icon"
:read_only
:label="t('vaultGroup.icon')"
:placeholder="t('vaultGroup.icon')"
/>
</div>
</VaultCardEdit>
</template>
<script setup lang="ts">
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router'
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const { t } = useI18n()
const showConfirmation = ref(false)
const vaultGroup = defineModel<SelectHaexPasswordsGroups>({ required: true })
const read_only = defineModel<boolean>('read_only')
const props = defineProps({
originally: Object as PropType<SelectHaexPasswordsGroups>,
})
defineEmits<{
submit: [to?: RouteLocationNormalizedLoadedGeneric]
close: [void]
back: [void]
reject: [to?: RouteLocationNormalizedLoadedGeneric]
}>()
const hasChanges = computed(() => {
console.log('group has changes', props.originally, vaultGroup.value)
if (!props.originally) {
if (
vaultGroup.value.color?.length ||
vaultGroup.value.description?.length ||
vaultGroup.value.icon?.length ||
vaultGroup.value.name?.length
) {
return true
} else {
return false
}
}
return JSON.stringify(props.originally) !== JSON.stringify(vaultGroup.value)
})
/* const onClose = () => {
if (props.originally) vaultGroup.value = { ...props.originally };
emit('close');
}; */
</script>
<i18n lang="json">
{
"de": {
"vaultGroup": {
"name": "Name",
"description": "Beschreibung",
"icon": "Icon",
"color": "Farbe"
}
},
"en": {
"vaultGroup": {
"name": "Name",
"description": "Description",
"icon": "Icon",
"color": "Color"
}
}
}
</i18n>

View File

@ -1,7 +0,0 @@
<template>
<UiList>
<slot />
</UiList>
</template>
<script setup lang="ts"></script>

View File

@ -1,77 +0,0 @@
<template>
<UiDialog
v-model:open="showConfirmation"
:title="t('dialog.title')"
>
{{ t('dialog.question') }}
<template #buttons>
<UiButton
class="btn-outline btn-error focus:bg-primary"
tabindex="10"
@click="$emit('reject')"
>
<Icon name="mdi:cancel" />
<span class="hidden sm:block">
{{ t('dialog.reject') }}
</span>
</UiButton>
<UiButton
ref="abortButtonRef"
class="btn-outline focus:bg-primary"
tabindex="11"
@click="showConfirmation = false"
>
<Icon name="mdi:close" />
<span class="hidden sm:block">
{{ t('dialog.abort') }}
</span>
</UiButton>
<UiButton
class="btn-outline btn-success"
tabindex="12"
@click="$emit('submit')"
>
<Icon name="mdi:check" />
<span class="hidden sm:block">
{{ t('dialog.save') }}
</span>
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
const showConfirmation = defineModel<boolean>()
const abortButtonRef = useTemplateRef('abortButtonRef')
const { t } = useI18n()
onUpdated(() => {
abortButtonRef.value?.$el.focus()
})
defineEmits(['submit', 'reject'])
</script>
<i18n lang="json">
{
"de": {
"dialog": {
"title": "Ungespeicherte Änderungen",
"question": "Möchten Sie die Änderungen speichern?",
"reject": "Verwerfen",
"abort": "Abbrechen",
"save": "Speichern"
}
},
"en": {
"dialog": {
"title": "Unsaved Changes",
"question": "Would you like to save the changes?",
"reject": "Reject",
"abort": "Abort",
"save": "Save"
}
}
}
</i18n>