removed haex-pass components

This commit is contained in:
2025-10-15 21:54:50 +02:00
parent 5d6acfef93
commit 033c9135c6
64 changed files with 2502 additions and 3659 deletions

View File

@ -62,11 +62,12 @@
class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400"
>
<div
v-if="isInstalled"
v-if="extension.isInstalled"
class="flex items-center gap-1 text-success font-medium"
>
<UIcon name="i-heroicons-check-circle-solid" />
<span>{{ t('installed') }}</span>
<span v-if="!extension.installedVersion">{{ t('installed') }}</span>
<span v-else>{{ t('installedVersion', { version: extension.installedVersion }) }}</span>
</div>
<div
v-if="extension.downloads"
@ -112,11 +113,11 @@
<template #footer>
<div class="flex items-center justify-between gap-2">
<UButton
:label="isInstalled ? t('installed') : t('install')"
:color="isInstalled ? 'neutral' : 'primary'"
:disabled="isInstalled"
:label="getInstallButtonLabel()"
:color="extension.isInstalled && !extension.installedVersion ? 'neutral' : 'primary'"
:disabled="extension.isInstalled && !extension.installedVersion"
:icon="
isInstalled ? 'i-heroicons-check' : 'i-heroicons-arrow-down-tray'
extension.isInstalled && !extension.installedVersion ? 'i-heroicons-check' : 'i-heroicons-arrow-down-tray'
"
size="sm"
@click.stop="$emit('install')"
@ -134,23 +135,10 @@
</template>
<script setup lang="ts">
interface MarketplaceExtension {
id: string
name: string
version: string
author?: string
description?: string
icon?: string
downloads?: number
rating?: number
verified?: boolean
tags?: string[]
downloadUrl?: string
}
import type { IMarketplaceExtension } from '~/types/haexhub'
defineProps<{
extension: MarketplaceExtension
isInstalled?: boolean
const props = defineProps<{
extension: IMarketplaceExtension
}>()
defineEmits(['click', 'install', 'details'])
@ -162,6 +150,16 @@ const formatNumber = (num: number) => {
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
return num.toString()
}
const getInstallButtonLabel = () => {
if (!props.extension.isInstalled) {
return t('install')
}
if (props.extension.installedVersion) {
return t('update')
}
return t('installed')
}
</script>
<i18n lang="yaml">
@ -169,12 +167,16 @@ de:
by: von
install: Installieren
installed: Installiert
installedVersion: 'Installiert (v{version})'
update: Aktualisieren
details: Details
verified: Verifiziert
en:
by: by
install: Install
installed: Installed
installedVersion: 'Installed (v{version})'
update: Update
details: Details
verified: Verified
</i18n>

View File

@ -1,107 +0,0 @@
<template>
<HaexPassCard
:title
@close="onClose"
>
<div class="flex flex-col gap-4 w-full p-4">
<slot />
<UiInput
v-show="!read_only"
v-model.trim="passwordGroup.name"
:label="t('group.name')"
:placeholder="t('group.name')"
:with-copy-button="read_only"
:read_only
autofocus
/>
<UiInput
v-show="!read_only || passwordGroup.description?.length"
v-model.trim="passwordGroup.description"
:read_only
:label="t('group.description')"
:placeholder="t('group.description')"
:with-copy-button="read_only"
/>
<UiSelectColor
v-model="passwordGroup.color"
:read_only
:label="t('group.color')"
:placeholder="t('group.color')"
/>
<UiSelectIcon
v-model="passwordGroup.icon"
:read_only
:label="t('group.icon')"
:placeholder="t('group.icon')"
/>
</div>
<slot name="footer" />
</HaexPassCard>
</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 passwordGroup = defineModel<SelectHaexPasswordsGroups>({ required: true })
const read_only = defineModel<boolean>('read_only')
const props = defineProps<{
originally: SelectHaexPasswordsGroups
title: string
}>()
defineEmits<{
submit: [to?: RouteLocationNormalizedLoadedGeneric]
close: [void]
back: [void]
reject: [to?: RouteLocationNormalizedLoadedGeneric]
}>()
const hasChanges = computed(() => {
console.log('group has changes', props.originally, passwordGroup.value)
if (!props.originally) {
if (
passwordGroup.value.color?.length ||
passwordGroup.value.description?.length ||
passwordGroup.value.icon?.length ||
passwordGroup.value.name?.length
) {
return true
} else {
return false
}
}
return (
JSON.stringify(props.originally) !== JSON.stringify(passwordGroup.value)
)
})
const onClose = () => {
/* if (props.originally) passwordGroup.value = { ...props.originally };
emit('close'); */
console.log('close group card')
}
</script>
<i18n lang="yaml">
de:
group:
name: Name
description: Beschreibung
icon: Icon
color: Farbe
en:
group:
name: Name
description: Description
icon: Icon
color: Color
</i18n>

View File

@ -1,12 +0,0 @@
<template>
<UiCard
:title
:icon
>
<slot />
</UiCard>
</template>
<script setup lang="ts">
defineProps<{ title: string; icon?: string }>()
</script>

View File

@ -1,46 +0,0 @@
<template>
<UiDialogConfirm
v-model:open="showConfirmDeleteDialog"
:confirm-label="final ? t('final.label') : t('label')"
:title="final ? t('final.title') : t('title', { itemName })"
@abort="$emit('abort')"
@confirm="$emit('confirm')"
>
<template #body>
{{
final ? t('final.question', { itemName }) : t('question', { itemName })
}}
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
const { t } = useI18n()
const showConfirmDeleteDialog = defineModel<boolean>('open')
defineProps<{ final?: boolean; itemName?: string | null }>()
defineEmits(['confirm', 'abort'])
</script>
<i18n lang="yaml">
de:
title: Eintrag löschen
question: Soll der Eintrag "{itemName}" in den Papierkorb verschoben werden?
label: Verschieben
final:
title: Eintrag endgültig löschen
question: Soll der Eintrag "{itemName}" endgültig gelöscht werden?
label: Löschen
en:
title: Delete Entry
question: Should the {itemName} entry be moved to the recycle bin?
label: Move
final:
title: Delete entry permanently
question: Should the entry {itemName} be permanently deleted?
label: Delete
</i18n>

View File

@ -1,51 +0,0 @@
<template>
<UiDialogConfirm
v-model:open="showUnsavedChangesDialog"
:confirm-label="t('label')"
:title="t('title')"
@abort="$emit('abort')"
@confirm="onConfirm"
>
<template #body>
<div class="flex items-center h-full">
{{ t('question') }}
</div>
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
const { t } = useI18n()
const showUnsavedChangesDialog = defineModel<boolean>('open')
const ignoreChanges = defineModel<boolean>('ignoreChanges')
const { hasChanges } = defineProps<{ hasChanges: boolean }>()
const emit = defineEmits(['confirm', 'abort'])
const onConfirm = () => {
ignoreChanges.value = true
emit('confirm')
}
onBeforeRouteLeave(() => {
if (hasChanges && !ignoreChanges.value) {
showUnsavedChangesDialog.value = true
return false
}
return true
})
</script>
<i18n lang="yaml">
de:
title: Nicht gespeicherte Änderungen
question: Sollen die Änderungen verworfen werden?
label: Verwerfen
en:
title: Unsaved changes
question: Should the changes be discarded?
label: Discard
</i18n>

View File

@ -1,59 +0,0 @@
<template>
<ul class="flex items-center gap-2 p-2">
<li>
<NuxtLinkLocale :to="{ name: 'passwordGroupItems' }">
<Icon
name="mdi:safe"
size="24"
/>
</NuxtLinkLocale>
</li>
<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 },
}"
>
<Icon name="mdi:pencil" />
</NuxtLinkLocale>
</UTooltip>
</li>
</ul>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const groups = defineProps<{ items: SelectHaexPasswordsGroups[] }>()
const lastGroup = computed(() => groups.items.at(-1))
const { t } = useI18n()
</script>
<i18n lang="yaml">
de:
edit: Bearbeiten
en:
edit: Edit
</i18n>

View File

@ -1,71 +0,0 @@
export const usePasswordGroup = () => {
const areItemsEqual = (
groupA: unknown | unknown[] | null,
groupB: unknown | unknown[] | null,
) => {
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)
if (groupA.length === groupB.length) return true
return groupA.some((group, index) => {
return areObjectsEqual(group, groupA[index])
})
}
return areObjectsEqual(groupA, groupB)
}
const deepEqual = (obj1: unknown, obj2: unknown) => {
console.log('compare values', obj1, obj2)
if (obj1 === obj2) return true
// Null/undefined Check
if (obj1 == null || obj2 == null) return obj1 === obj2
// Typ-Check
if (typeof obj1 !== typeof obj2) return false
// Primitive Typen
if (typeof obj1 !== 'object') return obj1 === obj2
// Arrays
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) return false
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i])) return false
}
return true
}
// Date Objekte
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
// RegExp Objekte
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// Objekte
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) return false
for (const key of keys1) {
if (!keys2.includes(key)) return false
if (!deepEqual(obj1[key], obj2[key])) return false
}
return true
}
return {
areItemsEqual,
deepEqual,
}
}

View File

@ -1,106 +0,0 @@
<template>
<UCard
v-if="group"
:ui="{ root: [''] }"
>
<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>
<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')"
/>
<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"
:readOnly
/>
<UiSelectColor
v-model="group.color"
:readOnly
/> -->
</div>
</form>
</UCard>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
const group = defineModel<SelectHaexPasswordsGroups | null>()
const { readOnly = false } = defineProps<{
readOnly?: boolean
mode: 'create' | 'edit'
}>()
const emit = defineEmits(['close', 'submit'])
const { t } = useI18n()
const nameRef = useTemplateRef('nameRef')
onStartTyping(() => {
nameRef.value?.$el.focus()
})
const { escape } = useMagicKeys()
watchEffect(async () => {
if (escape?.value) {
await nextTick()
emit('close')
}
})
</script>
<i18n lang="yaml">
de:
name: Name
description: Beschreibung
icon: Icon
color: Farbe
create: Erstellen
save: Speichern
abort: Abbrechen
title:
create: Gruppe erstellen
edit: Gruppe ändern
en:
name: Name
description: Description
icon: Icon
color: Color
create: Create
save: Save
abort: Abort
title:
create: Create group
edit: Edit group
</i18n>

View File

@ -1,127 +0,0 @@
<template>
<div class="h-full overflow-scroll">
<form
class="flex flex-col gap-4 w-full p-4"
@submit.prevent="$emit('submit')"
>
<UiInput
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
:with-copy-button
autofocus
@keyup.enter="$emit('submit')"
/>
<UiInput
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
@keyup.enter="$emit('submit')"
/>
<UiInputPassword
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="!readOnly"
class="join-item"
:password="itemDetails.password"
v-model="preventClose"
/> -->
</template>
</UiInputPassword>
<UiInputUrl
v-show="!readOnly || itemDetails.url"
v-model="itemDetails.url"
:label="t('item.url')"
:placeholder="t('item.url')"
:read-only
:with-copy-button
@keyup.enter="$emit('submit')"
/>
<!-- <UiSelectIcon
v-show="!readOnly"
:default-icon="defaultIcon || 'mdi:key-outline'"
:readOnly
v-model="itemDetails.icon"
/> -->
<UiTextarea
v-show="!readOnly || itemDetails.note"
v-model="itemDetails.note"
:label="t('item.note')"
:placeholder="t('item.note')"
:readOnly
:with-copy-button
@keyup.enter.stop
color="error"
/>
</form>
</div>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsItemDetails } from '~~/src-tauri/database/schemas/vault'
defineProps<{
defaultIcon?: string | null
readOnly?: boolean
withCopyButton?: boolean
}>()
defineEmits(['submit'])
const { t } = useI18n()
const itemDetails = defineModel<SelectHaexPasswordsItemDetails>({
required: true,
})
//const preventClose = defineModel<boolean>('preventClose')
const check = defineModel<boolean>('check-input', { default: false })
/* onKeyStroke('escape', (e) => {
e.stopPropagation()
e.stopImmediatePropagation()
}) */
const titleRef = useTemplateRef('titleRef')
onStartTyping(() => {
titleRef.value?.$el?.focus()
})
</script>
<i18n lang="yaml">
de:
item:
title: Titel
username: Nutzername
password: Passwort
url: Url
note: Notiz
en:
item:
title: Title
username: Username
password: Password
url: Url
note: Note
</i18n>

View File

@ -1,118 +0,0 @@
<template>
<div class="h-full overflow-scroll flex">
{{ _history }}
<UiList v-show="_history.length">
<!-- <UiListButton v-for="item in _history">
<div
class="flex items-start bg-slate-100 gap-x-2 w-full h-20 overflow-clip"
>
<div class="flex flex-col justify-between h-full py-2">
<h6 class="text-sm whitespace-nowrap bg-orange-200">
vorheriger {{ item.changedProperty }}
</h6>
<UiInput
:model-value="item.oldValue"
with-copy-button
/>
</div>
<div class="sm:flex flex-col justify-between h-full py-2 hidden">
<h6 class="text-sm">neuer Wert</h6>
<UiInput
:model-value="item.newValue"
with-copy-button
/>
</div>
<div class="flex flex-col justify-between h-full py-2">
<h6 class="text-sm md:text-base bg-orange-200">geändert_am</h6>
<span class="bg-red-100 py-1 md:py-2">
{{ item.createdAt }}
</span>
</div>
</div>
</UiListButton>
-->
</UiList>
<div
v-show="!_history.length"
class="content-center w-full text-center"
>
{{ t('noHistory') }}
</div>
</div>
<!-- <UiTable
v-if="history?.length"
:headers
:items="_history"
autofocus
>
<template #column-oldValue="{ item }: { item: string }">
<UiInput
:model-value="item"
with-copy-button
class="min-w-24"
/>
</template>
</UiTable> -->
</template>
<script setup lang="ts">
import type {
SelectHaexPasswordsGroupItems,
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
} from '~~/src-tauri/database/schemas/vault'
const history = defineModel<SelectHaexPasswordsItemHistory[]>()
const _history = computed(
() =>
history.value?.map((change) => ({
changedProperty: t(change.changedProperty!),
createdAt: new Date(change.createdAt!).toLocaleDateString(),
newValue: change.newValue,
oldValue: change.oldValue,
})) ?? [],
)
const { t } = useI18n()
interface ITableHeader {
label?: string
'item-value': string
}
const headers: ITableHeader[] = [
{ 'item-value': 'changedProperty', label: t('changedProperty') },
{ 'item-value': 'oldValue', label: t('oldValue') },
{ 'item-value': 'newValue', label: t('newValue') },
{ 'item-value': 'createdAt', label: t('createdAt') },
]
</script>
<i18n lang="json">
{
"de": {
"noHistory": "Eintrag wurde bisher nicht geändert",
"changedProperty": "Änderung",
"createdAt": "geändert am",
"newValue": "neuer Wert",
"oldValue": "alter Wert",
"password": "Passwort",
"title": "Titel",
"url": "Url",
"username": "Nutzername"
},
"en": {
"noHistory": "No changes so far",
"changedProperty": "Changes",
"createdAt": "changed at",
"newValue": "new Value",
"oldValue": "old Value",
"password": "Password",
"title": "Title",
"url": "Url",
"username": "Username"
}
}
</i18n>

View File

@ -1,182 +0,0 @@
<template>
<div class="p-1">
<UCard
class="rounded overflow-auto p-0 h-full"
@close="onClose"
>
<div class="">
<UTabs
:items="tabs"
variant="link"
:ui="{ trigger: 'grow' }"
class="gap-4 w-full"
>
<template #details>
<HaexPassItemDetails
v-if="details"
v-model="details"
with-copy-button
:read-only
:defaultIcon
v-model:prevent-close="preventClose"
@submit="$emit('submit')"
/>
</template>
<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"
class="h-full"
:aria-labelledby="id.details"
>
<HaexPassItemDetails
v-if="details"
v-model="details"
with-copy-button
:read_only
:defaultIcon
v-model:prevent-close="preventClose"
@submit="$emit('submit')"
/>
</div>
<div
id="tabs-basic-2"
class="hidden"
role="tabpanel"
:aria-labelledby="id.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"
/>
</div>
<div
id="tabs-basic-3"
class="hidden h-full"
role="tabpanel"
:aria-labelledby="id.history"
>
<HaexPassItemHistory />
</div>
</div> -->
</div>
</UCard>
</div>
</template>
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
SelectHaexPasswordsItemKeyValues,
} from '~~/src-tauri/database/schemas/vault'
defineProps<{
defaultIcon?: string | null
history: SelectHaexPasswordsItemHistory[]
}>()
const emit = defineEmits<{
close: []
addKeyValue: []
removeKeyValue: [string]
submit: []
}>()
const readOnly = defineModel<boolean>('readOnly', { default: false })
const details = defineModel<SelectHaexPasswordsItemDetails | null>('details', {
required: true,
})
const keyValues = defineModel<SelectHaexPasswordsItemKeyValues[]>('keyValues', {
default: [],
})
const keyValuesAdd = defineModel<SelectHaexPasswordsItemKeyValues[]>(
'keyValuesAdd',
{ default: [] },
)
const keyValuesDelete = defineModel<SelectHaexPasswordsItemKeyValues[]>(
'keyValuesDelete',
{ default: [] },
)
const { t } = useI18n()
/* const id = reactive({
details: useId(),
keyValue: useId(),
history: useId(),
content: {},
}) */
const preventClose = ref(false)
const onClose = () => {
if (preventClose.value) return
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">
{
"de": {
"create": "Anlegen",
"abort": "Abbrechen",
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "Verlauf"
}
},
"en": {
"create": "Create",
"abort": "Abort",
"tab": {
"details": "Details",
"keyValue": "Extra",
"history": "History"
}
}
}
</i18n>

View File

@ -1,126 +0,0 @@
<template>
<div class="p-4">
<div class="flex flex-wrap gap-2">
<UiList
v-if="items.length || itemsToAdd.length"
class="flex-1"
>
<li
v-for="item in [...items, ...itemsToAdd]"
:key="item.id"
:class="{ 'bg-primary/20': currentSelected === item }"
class="flex gap-2 hover:bg-primary/20 px-4 items-center"
@click="currentSelected = item"
>
<button class="flex items-center no-underline w-full py-2">
<input
v-model="item.key"
:readonly="currentSelected !== item || readOnly"
class="flex-1 cursor-pointer"
/>
</button>
<UiButton
v-if="!readOnly"
:class="[currentSelected === item ? 'visible' : 'invisible']"
variant="outline"
color="error"
icon="mdi:trash-outline"
@click="deleteItem(item.id)"
/>
</li>
</UiList>
<UTextarea
v-if="items.length || itemsToAdd.length"
:readOnly="readOnly || !currentSelected"
class="flex-1 min-w-52 border-base-content/25"
v-model="currentValue"
with-copy-button
/>
</div>
<div
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> -->
</UiButton>
</div>
</div>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsItemKeyValues } from '~~/src-tauri/database/schemas/vault'
const { itemId } = defineProps<{ readOnly?: boolean; itemId: string }>()
const items = defineModel<SelectHaexPasswordsItemKeyValues[]>({ default: [] })
const itemsToDelete = defineModel<SelectHaexPasswordsItemKeyValues[]>(
'itemsToDelete',
{ default: [] },
)
const itemsToAdd = defineModel<SelectHaexPasswordsItemKeyValues[]>(
'itemsToAdd',
{ default: [] },
)
defineEmits<{ add: []; remove: [string] }>()
//const { t } = useI18n()
const currentSelected = ref<SelectHaexPasswordsItemKeyValues | undefined>(
items.value?.at(0),
)
watch(
() => itemId,
() => (currentSelected.value = items.value?.at(0)),
)
//const currentValue = computed(() => currentSelected.value?.value || '')
const currentValue = computed({
get: () => currentSelected.value?.value || '',
set(newValue: string) {
if (currentSelected.value) currentSelected.value.value = newValue
},
})
const addItem = () => {
itemsToAdd.value?.push({
id: crypto.randomUUID(),
itemId,
key: '',
value: '',
updateAt: null,
haex_tombstone: null,
})
}
const deleteItem = (id: string) => {
const item = items.value.find((item) => item.id === id)
if (item) {
itemsToDelete.value?.push(item)
items.value = items.value.filter((item) => item.id !== id)
}
itemsToAdd.value = itemsToAdd.value?.filter((item) => item.id !== id) ?? []
}
</script>
<i18n lang="yaml">
de:
add: Hinzufügen
key: Schlüssel
value: Wert
en:
add: Add
key: Key
value: Value
</i18n>

View File

@ -1,92 +0,0 @@
<template>
<div
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')"
/>
</div>
<div>
<UiButton
v-show="showEditButton"
icon="mdi:pencil-outline"
class="pointer-events-auto"
size="xl"
:tooltip="t('edit')"
@click="$emit('edit')"
/>
<UiButton
v-show="showReadonlyButton"
icon="mdi:pencil-off-outline"
class="pointer-events-auto"
size="xl"
:tooltip="t('readonly')"
@click="$emit('readonly')"
/>
<UiButton
v-show="showSaveButton"
icon="mdi:content-save-outline"
size="xl"
class="pointer-events-auto"
:class="{ 'animate-pulse': hasChanges }"
:tooltip="t('save')"
@click="$emit('save')"
/>
</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')"
@click="$emit('delete')"
/>
</div>
</div>
</template>
<script setup lang="ts">
const { isVisible } = storeToRefs(useSidebarStore())
const { t } = useI18n()
defineProps<{
hasChanges?: boolean
showCloseButton?: boolean
showDeleteButton?: boolean
showEditButton?: boolean
showReadonlyButton?: boolean
showSaveButton?: boolean
}>()
defineEmits(['close', 'edit', 'readonly', 'save', 'delete'])
</script>
<i18n lang="yaml">
de:
save: Speichern
abort: Abbrechen
edit: Bearbeiten
readonly: Lesemodus
delete: Löschen
en:
save: Save
abort: Abort
edit: Edit
readonly: Read Mode
delete: Delete
</i18n>

View File

@ -1,128 +0,0 @@
<template>
<div
v-if="menuItems?.length"
class="flex-1 h-full"
>
<ul
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"
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-elevated/30 outline outline-accent hover:bg-base-content/20':
selectedItems.has(item) ||
(currentSelectedItem?.id === item.id &&
longPressedHook &&
!selectedItems.has(item)),
'opacity-60 shadow-accent': selectedGroupItems?.some(
(_item) => _item.id === item.id,
),
}"
:style="{ '--motion-delay': `${50 * index}ms` }"
@mousedown="
longPressedHook
? (currentSelectedItem = null)
: (currentSelectedItem = item)
"
>
<HaexPassMobileMenuItem
v-bind="item"
@click="onClickItemAsync(item)"
/>
</li>
</ul>
</div>
<div
v-else
class="flex justify-center items-center flex-1"
>
<UiIconNoData class="text-primary size-24 shrink-0" />
</div>
</template>
<script setup lang="ts">
import { vOnLongPress } from '@vueuse/components'
import type { IPasswordMenuItem } from './types'
defineProps<{
menuItems: IPasswordMenuItem[]
}>()
defineEmits(['add'])
const selectedItems = defineModel<Set<IPasswordMenuItem>>('selectedItems', {
default: new Set(),
})
const currentSelectedItem = ref<IPasswordMenuItem | null>()
const longPressedHook = ref(false)
const onLongPressCallbackHook = (_: PointerEvent) => {
longPressedHook.value = true
}
watch(longPressedHook, () => {
if (!longPressedHook.value) selectedItems.value.clear()
})
watch(selectedItems, () => {
if (!selectedItems.value.size) longPressedHook.value = false
})
const localePath = useLocalePath()
const { ctrl } = useMagicKeys()
const { search } = storeToRefs(useSearchStore())
const onClickItemAsync = async (item: IPasswordMenuItem) => {
currentSelectedItem.value = null
if (longPressedHook.value || selectedItems.value.size || ctrl?.value) {
if (selectedItems.value?.has(item)) {
selectedItems.value.delete(item)
} else {
selectedItems.value?.add(item)
}
if (!selectedItems.value.size) longPressedHook.value = false
} else {
if (item.type === 'group')
await navigateTo(
localePath({
name: 'passwordGroupItems',
params: {
...useRouter().currentRoute.value.params,
groupId: item.id,
},
}),
)
else {
await navigateTo(
localePath({
name: 'passwordItemEdit',
params: { ...useRouter().currentRoute.value.params, itemId: item.id },
}),
)
}
search.value = ''
}
}
const listRef = useTemplateRef('listRef')
onClickOutside(listRef, async () => {
// needed cause otherwise the unselect is to fast for other processing like "edit selected group"
setTimeout(() => {
longPressedHook.value = false
}, 50)
})
const { selectedGroupItems } = storeToRefs(usePasswordGroupStore())
</script>

View File

@ -1,39 +0,0 @@
<template>
<button
class="flex gap-4 w-full px-4 py-2"
:style="{ color: menuItem.color ?? '' }"
@click="$emit('click', menuItem)"
>
<Icon
:name="menuIcon"
size="24"
class="shrink-0"
/>
<p class="w-full flex-1 text-start truncate font-bold">
{{ menuItem?.name }}
</p>
<Icon
v-if="menuItem.type === 'group'"
name="mdi:chevron-right"
size="24"
class="text-base-content"
/>
</button>
</template>
<script setup lang="ts">
import type { IPasswordMenuItem } from './types'
defineEmits<{ click: [group?: IPasswordMenuItem] }>()
const menuItem = defineProps<IPasswordMenuItem>()
const menuIcon = computed(() =>
menuItem?.icon
? menuItem.icon
: menuItem.type === 'group'
? 'mdi:folder-outline'
: 'mdi:key-outline',
)
</script>

View File

@ -1,7 +0,0 @@
export interface IPasswordMenuItem {
color?: string | null
icon: string | null
id: string
name: string | null
type: 'group' | 'item'
}

View File

@ -27,6 +27,11 @@ const items: DropdownMenuItem[] = [
label: t('settings'),
to: useLocalePath()({ name: 'settings' }),
},
{
icon: 'mdi:code-braces',
label: t('developer'),
to: useLocalePath()({ name: 'settings-developer' }),
},
{
icon: 'tabler:logout',
label: t('close'),
@ -39,9 +44,11 @@ const items: DropdownMenuItem[] = [
<i18n lang="yaml">
de:
settings: 'Einstellungen'
developer: 'Entwickler'
close: 'Vault schließen'
en:
settings: 'Settings'
developer: 'Developer'
close: 'Close Vault'
</i18n>