mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
switch to nuxt ui
This commit is contained in:
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
56
src/components/haex/menu/applications.vue
Normal file
56
src/components/haex/menu/applications.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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[] }>()
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
8
src/components/ui/button/types.d.ts
vendored
8
src/components/ui/button/types.d.ts
vendored
@ -1,8 +0,0 @@
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export interface IActionMenuItem {
|
||||
label: string
|
||||
icon?: string
|
||||
action?: () => Promise<unknown>
|
||||
to?: RouteLocationRaw
|
||||
}
|
||||
@ -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>
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
45
src/components/ui/dropdown/vault.vue
Normal file
45
src/components/ui/dropdown/vault.vue
Normal 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>
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
60
src/components/ui/sidebar/link.vue
Normal file
60
src/components/ui/sidebar/link.vue
Normal 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>
|
||||
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<input v-model="value" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const value = defineModel()
|
||||
</script>
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div data-tree-view role="tree" aria-orientation="vertical" class="rounded min-w-fit w-full">
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
19
src/components/ui/tree/types.d.ts
vendored
19
src/components/ui/tree/types.d.ts
vendored
@ -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
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -1 +0,0 @@
|
||||
<template><div>first time</div></template>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<UiList>
|
||||
<slot />
|
||||
</UiList>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
@ -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>
|
||||
Reference in New Issue
Block a user