init commit

This commit is contained in:
Martin Drechsel
2025-04-02 18:54:55 +02:00
commit 2c5ec6b281
126 changed files with 21323 additions and 0 deletions

View File

@ -0,0 +1,155 @@
<template>
<div class="browser">
<div class="browser-controls">
<button
@click="$emit('goBack', activeTabId)"
:disabled="!activeTabId"
>
</button>
<button
@click="$emit('goForward', activeTabId)"
:disabled="!activeTabId"
>
</button>
<button @click="$emit('createTab')">+</button>
<HaexBrowserUrlBar
:url="activeTab?.url || ''"
:isLoading="activeTab?.isLoading || false"
@submit="handleUrlSubmit"
/>
</div>
<HaexBrowserTabBar
:tabs="tabs"
:activeTabId="activeTabId"
@closeTab="$emit('closeTab', $event)"
@activateTab="$emit('activateTab', $event)"
/>
<div
class="browser-content"
ref="contentRef"
>
<!-- 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 { invoke } from '@tauri-apps/api/core';
import { Window } from '@tauri-apps/api/window';
import { getCurrentWebview, Webview } from '@tauri-apps/api/webview';
/* 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 handleUrlSubmit = (url: string) => {
if (props.activeTabId) {
// Prüfe URL mit Erweiterungen vor der Navigation
if (processNavigation(url)) {
emit('navigate', props.activeTabId, url);
} else {
console.log('Navigation blockiert durch Erweiterung');
// Hier könnten Sie eine Benachrichtigung anzeigen
}
}
};
const updateWebviewBoundsAsync = async () => {
if (!contentRef.value) return;
const rect = contentRef.value.getBoundingClientRect();
const bounds = {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
};
/* await invoke('update_window_bounds', {
contentBounds: { x: bounds.x, y: bounds.y },
contentSize: { width: bounds.width, height: bounds.height },
}); */
};
</script>

View File

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

View File

@ -0,0 +1,57 @@
<template>
<form
class="url-bar"
@submit.prevent="handleSubmit"
>
<input
type="text"
v-model="inputValue"
placeholder="URL eingeben"
/>
<span
v-if="isLoading"
class="loading-indicator"
>Laden...</span
>
<button
v-else
type="submit"
>
Go
</button>
</form>
</template>
<script setup lang="ts">
const props = defineProps({
url: {
type: String,
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['submit']);
const inputValue = ref(props.url);
watch(
() => props.url,
(newUrl) => {
inputValue.value = newUrl;
}
);
const handleSubmit = () => {
// URL validieren und ggf. Protokoll hinzufügen
let processedUrl = inputValue.value.trim();
if (processedUrl && !processedUrl.match(/^[a-zA-Z]+:\/\//)) {
processedUrl = 'https://' + processedUrl;
}
emit('submit', processedUrl);
};
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="fixed z-10">
<div
class="dropdown relative inline-flex [--placement:top] [--strategy:absolute]"
>
<button
:id
class="dropdown-toggle btn btn-primary btn-lg btn-square dropdown-open:rotate-45"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Menu"
>
<Icon
:name="icon"
size="46"
/>
</button>
<div
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60 rtl:left-0 bg-transparent"
role="menu"
aria-orientation="vertical"
:aria-labelledby="id"
>
<ul
class="dropdown-open:ease-in dropdown-open:translate-x-0 -translate-x-1 rtl:translate-x-1 transition duration-300 ease-out"
data-dropdown-transition
>
<li
v-for="link in menu"
class="dropdown-item hover:bg-transparent"
>
<NuxtLinkLocale
v-if="link.to"
:to="link.to"
class="btn btn-primary flex items-center no-underline rounded-lg flex-nowrap"
>
<Icon
v-if="link.icon"
:name="link.icon"
class="me-3"
/>
{{ 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>
</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();
</script>

View File

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

8
src/components/ui/button/types.d.ts vendored Normal file
View File

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

View File

@ -0,0 +1,121 @@
<template>
<slot
name="trigger"
:id
>
</slot>
<div
:id
class="overlay modal overlay-open:opacity-100 hidden modal-middle [--tab-accessibility-limited:false] overflow-scroll p-0 sm:p-4"
role="dialog"
ref="modalRef"
>
<div
class="overlay-animation-target overlay-open:mt-4 overlay-open:duration-500 mt-12 transition-all ease-out modal-dialog overlay-open:opacity-100"
>
<div class="modal-content">
<div class="modal-header">
<slot name="title">
<h3
v-if="title"
class="modal-title text-base sm:text-lg"
>
{{ title }}
</h3>
</slot>
<button
type="button"
class="btn btn-text btn-circle btn-sm absolute end-3 top-3"
:aria-label="t('close')"
@click="open = false"
tabindex="1"
>
<Icon
name="mdi:close"
size="18"
/>
</button>
</div>
<div class="modal-body text-sm sm:text-base py-1">
<slot />
</div>
<div class="modal-footer flex-wrap">
<slot name="buttons" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { HSOverlay } from 'flyonui/flyonui';
export interface IDom {
class?: String;
text: String;
}
const id = useId();
defineProps({
trigger: {
type: Object as PropType<IDom>,
default: () => ({
class: '',
text: '',
}),
},
title: {
type: String,
default: '',
},
description: {
type: Object as PropType<IDom>,
default: () => ({
class: '',
text: '',
}),
required: false,
},
});
const open = defineModel<boolean>('open', { default: false });
const { t } = useI18n();
const modalRef = useTemplateRef('modalRef');
const modal = ref<HSOverlay>();
watch(open, async () => {
if (open.value) {
//console.log('open modal', modal.value?.open);
await modal.value?.open();
} else {
await modal.value?.close(true);
}
});
onMounted(() => {
if (!modalRef.value) return;
modal.value = new HSOverlay(modalRef.value, { isClosePrev: true });
modal.value.on('close', () => {
console.log('close it from event', open.value);
open.value = false;
});
});
</script>
<i18n lang="json">
{
"de": {
"close": "Schließen"
},
"en": {
"close": "Close"
}
}
</i18n>

View File

@ -0,0 +1,58 @@
<template>
<button
type="button"
class="btn btn-primary"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="basic-modal"
data-overlay="#basic-modal"
>
Open modal
</button>
<div
id="basic-modal"
class="overlay modal overlay-open:opacity-100 hidden"
role="dialog"
tabindex="-1"
>
<div class="modal-dialog overlay-open:opacity-100">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Dialog Title</h3>
<button
type="button"
class="btn btn-text btn-circle btn-sm absolute end-3 top-3"
aria-label="Close"
data-overlay="#basic-modal"
>
<span class="icon-[tabler--x] size-4"></span>
</button>
</div>
<div class="modal-body">
This is some placeholder content to show the scrolling behavior for
modals. Instead of repeating the text in the modal, we use an inline
style to set a minimum height, thereby extending the length of the
overall modal and demonstrating the overflow scrolling. When content
becomes longer than the height of the viewport, scrolling will move
the modal as needed.
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-soft btn-secondary"
data-overlay="#basic-modal"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
>
Save changes
</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<h3 class="modal-title">
<slot />
</h3>
</template>

View File

@ -0,0 +1,199 @@
<template>
<span>
<fieldset class="join w-full">
<slot name="prepend" />
<span class="input-group join-item">
<span
v-if="prependIcon || prependLabel"
class="input-group-text"
>
<label v-if="prependLabel">
{{ prependLabel }}
</label>
<Icon :name="prependIcon" />
</span>
<div class="relative w-full">
<input
:id
:name="name ?? id"
:placeholder="placeholder || label"
:type
:autofocus
class="input input-floating peer join-item"
:class="{
'input-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
v-bind="$attrs"
v-model="input"
ref="inputRef"
:readonly="read_only"
/>
<label
v-if="label"
:for="id"
class="input-floating-label"
>
{{ label }}
</label>
</div>
<span
v-if="appendIcon || appendLabel"
class="input-group-text"
>
<label
v-if="appendLabel"
class=""
>
{{ appendLabel }}
</label>
<Icon :name="appendIcon" />
</span>
</span>
<slot name="append" />
<UiButton
v-if="withCopyButton"
class="btn-outline btn-accent h-auto"
@click="copy(`${input}`)"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
<!-- <button
v-if="withCopyButton"
class="btn btn-outline btn-accent join-item h-auto"
:class="{
'btn-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
@click="copy(`${input}`)"
type="button"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</button> -->
</fieldset>
<span
class="flex flex-col px-2 pt-0.5"
v-show="errors"
>
<span
v-for="error in errors"
class="label-text-alt text-error"
>
{{ error }}
</span>
</span>
</span>
</template>
<script setup lang="ts">
import { type ZodSchema } from 'zod';
const inputRef = useTemplateRef('inputRef');
defineExpose({ inputRef });
defineOptions({
inheritAttrs: false,
});
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,
autofocus: Boolean,
read_only: Boolean,
});
const input = defineModel<string | number | undefined | null>({
default: '',
required: true,
});
const { currentScreenSize } = storeToRefs(useUiStore());
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 emit = defineEmits(['error']);
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 = [];
}
}
};
const { copy, copied } = useClipboard();
</script>

View File

@ -0,0 +1,54 @@
<template>
<UiInput
:check-input
:label="label || t('password')"
:placeholder="placeholder || t('password')"
:rules
:type="type"
:autofocus
v-model="value"
>
<template #append>
<UiButton
class="btn-outline btn-accent h-auto"
@click="tooglePasswordType"
>
<Icon :name="type === 'password' ? 'mdi:eye' : 'mdi:eye-off'" />
</UiButton>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod';
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
const value = defineModel<string | number | null | undefined>();
defineProps({
label: String,
placeholder: String,
checkInput: Boolean,
rules: Object as PropType<ZodSchema>,
autofocus: Boolean,
});
const type = ref<'password' | 'text'>('password');
const tooglePasswordType = () => {
type.value = type.value === 'password' ? 'text' : 'password';
};
</script>
<i18n lang="json">
{
"de": {
"password": "Passwort"
},
"en": {
"password": "Password"
}
}
</i18n>

View File

@ -0,0 +1,56 @@
<template>
<UiInput
:autofocus
:check-input="checkInput"
:label="label || t('url')"
:placeholder="placeholder || t('url')"
:read_only
:rules
:with-copy-button
v-model.trim="value"
>
<template #append>
<UiButton
v-if="read_only"
@click="openUrl(`${value}`)"
class="btn-outline btn-accent h-auto"
:class="{
disabled: !value?.length,
}"
>
<Icon name="streamline:web" />
</UiButton>
</template>
</UiInput>
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod';
import { openUrl } from '@tauri-apps/plugin-opener';
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
const value = defineModel<string | null | undefined>();
defineProps({
label: String,
placeholder: String,
checkInput: Boolean,
rules: Object as PropType<ZodSchema>,
autofocus: Boolean,
withCopyButton: Boolean,
read_only: Boolean,
});
</script>
<i18n lang="json">
{
"de": {
"url": "Url"
},
"en": {
"url": "Url"
}
}
</i18n>

View File

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

View File

@ -0,0 +1,24 @@
<template>
<UiTooltip
:tooltip="tooltip ?? label"
direction="right-end"
>
<button
class="link flex items-center justify-center py-3 hover:text-primary tooltip-toogle bg w-full"
@click="$emit('click')"
>
<Icon
:name="icon"
class="size-8"
/>
</button>
</UiTooltip>
</template>
<script setup lang="ts">
defineProps<{
label: string;
tooltip?: string;
icon: string;
}>();
</script>

View File

@ -0,0 +1,109 @@
<template>
<aside
class="flex shrink-0 transition-[width] ease-in duration-300 z-30 h-full overflow-hidden fixed sm:relative left-0 shadow border-r border-base-300"
>
<div class="sm:flex flex-col w-14 bg-base-200 shrink-0 h-full hidden">
<img
src="/logo.svg"
class="bg-primary p-3 size-16"
/>
<div class="flex flex-col justify-between h-full overflow-y-scroll z-10">
<div class="flex flex-col space-y-2 text-base-content/90">
<template v-for="item in menu.top">
<UiSidebarLink
v-if="item.to"
:to="item.to ?? ''"
:icon="item.icon"
:label="$t(item.label)"
/>
<UiSidebarButton
v-else
:icon="item.icon"
:label="$t(item.label)"
@click="item.click"
/>
</template>
</div>
<div class="flex flex-col space-y-2 text-base-content/90">
<template v-for="item in menu.bottom">
<UiSidebarLink
v-if="item.to"
:to="item.to ?? ''"
:icon="item.icon"
:label="$t(item.label)"
/>
<UiSidebarButton
v-else
:icon="item.icon"
:label="$t(item.label)"
@click="item.click"
/>
</template>
<!-- <UiSidebarLink
v-for="item in menu.bottom"
:icon="item.icon"
:to="item.to ?? ''"
:label="item.label"
@click="item.click"
/> -->
</div>
</div>
</div>
<!-- <div class="bg-base-100 flex flex-col w-full overflow-clip">
<div
class="h-16 flex items-center sm:justify-center justify-end md:justify-start bg-base-300 shrink-0"
>
<button
class="top-3 left-2 absolute z-30 duration-1000 btn btn-square btn-primary transition-opacity btn-outline sm:hidden"
@click="show = !show"
>
<Icon
name="mdi:menu"
size="28"
/>
</button>
<span
class="px-4 font-semibold text-base-content shrink-0 sm:bg-transparent bg-primary h-full flex items-center rounded-l-lg"
>
<p>Haex Vault</p>
</span>
<img
src="/logo.svg"
class="bg-primary p-3 size-16 shrink-0 sm:hidden rounded-r-lg"
/>
<button
class="btn btn-square btn-primary btn-outline mr-2 ml-auto hidden sm:flex"
@click="show = false"
>
<Icon
name="mdi:close"
size="28"
/>
</button>
</div>
<div class="overflow-scroll flex pb-4 relative">
<slot />
</div>
</div> -->
</aside>
</template>
<script lang="ts" setup>
defineProps({
menu: {
type: Object as PropType<ISidebarMenu>,
default: () => {},
},
});
//const show = ref(true);
const { show } = storeToRefs(useSidebarStore());
</script>

View File

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

View File

@ -0,0 +1,268 @@
<template>
<nav
class="navbar bg-base-100 max-sm:rounded-box max-sm:shadow sm:border-b border-base-content/25 sm:z-[1] relative"
>
<button
type="button"
class="btn btn-text max-sm:btn-square sm:hidden me-2"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
data-overlay="#sidebar"
>
<span class="icon-[tabler--menu-2] size-5"></span>
</button>
<div class="flex flex-1 items-center">
<a
class="link text-base-content link-neutral text-xl font-semibold no-underline"
href="#"
>
<UiTextGradient>Haex Hub</UiTextGradient>
</a>
</div>
<div class="navbar-end flex items-center gap-4">
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle btn btn-text btn-circle dropdown-open:bg-base-content/10 size-10"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="indicator">
<span
v-show="notifications.length"
class="indicator-item bg-error size-2 rounded-full text-sm"
></span>
<span
class="icon-[tabler--bell] text-base-content size-[1.375rem]"
></span>
</div>
</button>
<div
class="dropdown-menu dropdown-open:opacity-100 hidden"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-scrollable"
>
<div class="dropdown-header justify-center">
<h6 class="text-base-content text-base">
{{ t('notifications.label') }}
</h6>
</div>
<div
class="vertical-scrollbar horizontal-scrollbar rounded-scrollbar text-base-content/80 max-h-56 overflow-auto max-md:max-w-60"
>
<div
class="dropdown-item"
v-for="notification in notifications"
>
<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.description }}
</small>
</div>
</div>
</div>
<a
href="#"
class="dropdown-footer justify-center gap-1"
>
<span class="icon-[tabler--eye] size-4"></span>
{{ t('notifications.view_all') }}
</a>
</div>
</div>
<div
class="dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end]"
>
<button
id="dropdown-scrollable"
type="button"
class="dropdown-toggle flex items-center"
aria-haspopup="menu"
aria-expanded="false"
aria-label="Dropdown"
>
<div class="avatar">
<div class="size-9.5 rounded-full">
<img
src="https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png"
alt="avatar 1"
/>
</div>
</div>
</button>
<ul
class="dropdown-menu dropdown-open:opacity-100 hidden min-w-60"
role="menu"
aria-orientation="vertical"
aria-labelledby="dropdown-avatar"
>
<li class="dropdown-header gap-2">
<div class="avatar">
<div class="w-10 rounded-full">
<img
src="https://cdn.flyonui.com/fy-assets/avatar/avatar-1.png"
alt="avatar"
/>
</div>
</div>
<div>
<h6 class="text-base-content text-base font-semibold">
John Doe
</h6>
<small class="text-base-content/50">Admin</small>
</div>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--user]"></span>
My Profile
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--settings]"></span>
Settings
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--receipt-rupee]"></span>
Billing
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
>
<span class="icon-[tabler--help-triangle]"></span>
FAQs
</a>
</li>
<li class="dropdown-footer gap-2">
<a
class="btn btn-error btn-soft btn-block"
href="#"
>
<span class="icon-[tabler--logout]"></span>
Sign out
</a>
</li>
</ul>
</div>
</div>
</nav>
<aside
id="sidebar"
class="overlay sm:shadow-none overlay-open:translate-x-0 drawer drawer-start hidden max-w-64 sm:absolute sm:z-0 sm:flex sm:translate-x-0 pt-16"
role="dialog"
tabindex="-1"
>
<div class="drawer-body px-2 pt-4">
<ul class="menu p-0">
<li v-for="item in menu">
<a href="#">
<span class="icon-[tabler--home] size-5"></span>
Home
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--user] size-5"></span>
Account
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--message] size-5"></span>
Notifications
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--mail] size-5"></span>
Email
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--calendar] size-5"></span>
Calendar
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--shopping-bag] size-5"></span>
Product
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--login] size-5"></span>
Sign In
</a>
</li>
<li>
<a href="#">
<span class="icon-[tabler--logout-2] size-5"></span>
Sign Out
</a>
</li>
</ul>
</div>
</aside>
</template>
<script setup lang="ts">
const { t } = useI18n();
const { notifications } = storeToRefs(useNotificationStore());
const { menu } = storeToRefs(useSidebarStore());
</script>
<i18n lang="yaml">
de:
notifications:
label: Benachrichtigungen
view_all: Alle ansehen
en:
notifications:
label: Notifications
view_all: View all
</i18n>

View File

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

View File

@ -0,0 +1,61 @@
<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 z-40"
role="tooltip"
>
<span
class="tooltip-body"
v-bind="$attrs"
>
{{ 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: String,
trigger: {
type: String as PropType<'focus' | 'hover' | 'click'>,
default: 'hover',
},
});
defineOptions({
inheritAttrs: false,
});
</script>

View File

@ -0,0 +1,165 @@
<template>
<UiDialog
:title="t('title')"
v-model:open="open"
>
<template #trigger="{ id }">
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap"
@click="open = true"
>
<Icon name="mdi:plus" />
{{ t('database.create') }}
</button>
</template>
<form
class="flex flex-col gap-4"
@submit="onCreateAsync"
>
<!-- @keyup.enter="onCreateAsync" -->
<UiInput
:check-input="check"
:label="t('database.label')"
:placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name"
autofocus
prepend-icon="mdi:safe"
v-model="database.name"
/>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
</form>
<template #buttons>
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
class="btn-primary"
@click="onCreateAsync"
>
{{ t('create') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { save } from '@tauri-apps/plugin-dialog';
import { useVaultStore } from '~/stores/vault';
import { vaultDatabaseSchema } from './schema';
const check = ref(false);
const open = ref();
const { t } = useI18n();
const database = reactive<{
name: string;
password: string;
path: string | null;
type: 'password' | 'text';
}>({
name: '',
password: '',
path: '',
type: 'password',
});
const initDatabase = () => {
database.name = t('database.name');
database.password = '';
database.path = '';
database.type = 'password';
};
initDatabase();
const { add } = useSnackbar();
const { createAsync } = useVaultStore();
//const { show } = storeToRefs(useSidebarStore());
const onCreateAsync = async () => {
check.value = true;
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name);
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
);
console.log(
'checks',
database.name,
nameCheck,
database.password,
passwordCheck
);
if (!nameCheck.success || !passwordCheck.success) return;
open.value = false;
try {
database.path = await save({ defaultPath: `${database.name}.db` });
console.log('data', database);
if (database.path && database.password) {
const vaultId = await createAsync({
path: database.path,
password: database.password,
});
//show.value = true;
await navigateTo(
useLocaleRoute()({ name: 'vault', params: { vaultId } })
);
}
} catch (error) {
console.error(error);
add({ type: 'error', text: JSON.stringify(error) });
}
};
const onClose = () => {
open.value = false;
initDatabase();
};
</script>
<i18n lang="json">
{
"de": {
"database": {
"label": "Datenbankname",
"placeholder": "Passwörter",
"create": "Neue Vault anlegen",
"name": "Passwörter"
},
"title": "Neue Datenbank anlegen",
"create": "Erstellen",
"abort": "Abbrechen",
"description": "Haex Vault für deine geheimsten Geheimnisse"
},
"en": {
"database": {
"label": "Databasename",
"placeholder": "Databasename",
"create": "Create new Vault",
"name": "Passwords"
},
"title": "Create New Database",
"create": "Create",
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}
</i18n>

View File

@ -0,0 +1,179 @@
<template>
<UiDialog v-model:open="isOpen">
<!-- @close="initDatabase" -->
<template #trigger>
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1"
@click="onLoadDatabase"
>
<Icon name="mdi:folder-open-outline" />
{{ t('database.open') }}
</button>
</template>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
@keyup.enter="onOpenDatabase"
autofocus
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
<template #buttons>
<UiButton
class="btn-error"
@click="onClose"
>
{{ t('abort') }}
</UiButton>
<UiButton
type="submit"
class="btn-primary"
@click="onOpenDatabase"
>
{{ t('open') }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { open } from '@tauri-apps/plugin-dialog';
import { vaultDatabaseSchema } from './schema';
const { t } = useI18n();
const isOpen = defineModel('isOpen', { type: Boolean });
const props = defineProps({
path: String,
});
const check = ref(false);
const database = reactive<{
name: string;
password: string;
path: string | null;
type: 'password' | 'text';
}>({
name: '',
password: '',
path: '',
type: 'password',
});
const initDatabase = () => {
database.name = '';
database.password = '';
database.path = '';
database.type = 'password';
};
initDatabase();
const { add } = useSnackbar();
const handleError = (error: unknown) => {
isOpen.value = false;
add({ type: 'error', text: JSON.stringify(error) });
//console.error(error);
};
const { openAsync } = useVaultStore();
//const { show } = storeToRefs(useSidebarStore());
const onLoadDatabase = async () => {
try {
database.path = await open({
multiple: false,
directory: false,
filters: [
{
name: 'HaexVault',
extensions: ['db'],
},
],
});
if (!database.path) return;
isOpen.value = true;
} catch (error) {
handleError(error);
}
};
const localePath = useLocalePath();
const onOpenDatabase = async () => {
try {
check.value = true;
const path = database.path || props.path;
const pathCheck = vaultDatabaseSchema.path.safeParse(path);
const passwordCheck = vaultDatabaseSchema.password.safeParse(
database.password
);
if (!pathCheck.success || !passwordCheck.success || !path) {
add({ type: 'error', text: 'params falsch' });
return;
}
//console.log('try to open', path);
const vaultId = await openAsync({
path,
password: database.password,
});
if (!vaultId) {
add({ type: 'error', text: 'Vault konnte nicht geöffnet werden' });
return;
}
onClose();
/* await navigateTo(
localePath({
name: 'vaultGroup',
params: {
vaultId,
},
query: {
showSidebar: 'true',
},
})
); */
} catch (error) {
console.log(error);
handleError(error);
}
};
const onClose = () => {
initDatabase();
isOpen.value = false;
};
</script>
<i18n lang="json">
{
"de": {
"open": "Öffnen",
"abort": "Abbrechen",
"database": {
"open": "Vault öffnen"
}
},
"en": {
"open": "Open",
"abort": "Abort",
"database": {
"open": "Open Vault"
}
}
}
</i18n>

View File

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

View File

@ -0,0 +1,250 @@
<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 flex-col items-center w-full min-h-14 gap-2 py-1"
:class="{ '-ml-6': !show }"
:style="{ color }"
>
<Icon
v-if="icon"
:name="icon"
size="28"
/>
<h5
v-show="read_only"
class="overflow-hidden whitespace-nowrap"
>
{{ 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"
:class="{ 'pl-96': show }"
>
<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>
</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 { show } = storeToRefs(useSidebarStore());
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;
}
};
const onDelete = () => {};
onBeforeRouteLeave((_to, _from, next) => {
//console.log('check before leave', _to, _from);
to.value = _to;
if (isApprovedForLeave.value) {
isApprovedForLeave.value = false;
next();
} else if (props.hasChanges) {
showConfirmation.value = true;
} else {
next();
}
});
</script>
<i18n lang="json">
{
"de": {
"create": "Anlegen",
"abort": "Abbrechen",
"entry": {
"title": "Titel",
"username": "Nutzername",
"password": "Passwort",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "Verlauf"
}
},
"en": {
"create": "Create",
"abort": "Abort",
"entry": {
"title": "Title",
"username": "Username",
"password": "Password",
"url": "Url"
},
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "History"
}
}
}
</i18n>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,78 @@
<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
class="btn-outline focus:bg-primary"
tabindex="11"
ref="abortButtonRef"
@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();
const { currentScreenSize } = storeToRefs(useUiStore());
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>