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

@ -57,14 +57,58 @@
:style="{ display: tab.isVisible && !showConsole ? 'block' : 'none' }"
class="absolute inset-0"
>
<!-- Error overlay for dev extensions when server is not reachable -->
<div
v-if="tab.extension.devServerUrl && iframe.errors[tab.extension.id]"
class="absolute inset-0 bg-base-100 flex items-center justify-center p-8"
>
<div class="max-w-md space-y-4 text-center">
<Icon
name="mdi:alert-circle-outline"
size="64"
class="mx-auto text-warning"
/>
<h3 class="text-lg font-semibold">
{{ t('devServer.notReachable.title') }}
</h3>
<p class="text-sm opacity-70">
{{
t('devServer.notReachable.description', {
url: tab.extension.devServerUrl,
})
}}
</p>
<div class="bg-base-200 p-4 rounded text-left text-xs font-mono">
<p class="opacity-70 mb-2">
{{ t('devServer.notReachable.howToStart') }}
</p>
<code class="block">cd /path/to/extension</code>
<code class="block">npm run dev</code>
</div>
<UButton
:label="t('devServer.notReachable.retry')"
@click="retryLoadIFrame(tab.extension.id)"
/>
</div>
</div>
<iframe
:ref="
(el) => registerIFrame(tab.extension.id, el as HTMLIFrameElement)
"
class="w-full h-full border-0"
:src="getExtensionUrl(tab.extension)"
sandbox="allow-scripts allow-storage-access-by-user-activation allow-forms"
:src="
getExtensionUrl(
tab.extension.publicKey,
tab.extension.name,
tab.extension.version,
'index.html',
tab.extension.devServerUrl ?? undefined,
)
"
:sandbox="iframe.sandboxAttributes(tab.extension.devServerUrl)"
allow="autoplay; speaker-selection; encrypted-media;"
@error="onIFrameError(tab.extension.id)"
/>
</div>
@ -130,7 +174,8 @@
log.level === 'info' ? 'text-info' : '',
log.level === 'debug' ? 'text-base-content/70' : '',
]"
>{{ log.message }}</pre>
>{{ log.message }}</pre
>
</div>
</div>
@ -156,16 +201,8 @@
<script setup lang="ts">
import {
useExtensionMessageHandler,
registerExtensionIFrame,
unregisterExtensionIFrame,
} from '~/composables/extensionMessageHandler'
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
import type { IHaexHubExtension } from '~/types/haexhub'
import { platform } from '@tauri-apps/plugin-os'
import {
EXTENSION_PROTOCOL_NAME,
EXTENSION_PROTOCOL_PREFIX,
EXTENSION_PROTOCOL_NAME,
} from '~/config/constants'
definePageMeta({
@ -176,6 +213,65 @@ const { t } = useI18n()
const tabsStore = useExtensionTabsStore()
// Track iframe errors (for dev mode)
//const iframeErrors = ref<Record<string, boolean>>({})
const sandboxDefault = [
'allow-scripts',
'allow-storage-access-by-user-activation',
'allow-forms',
] as const
const iframe = reactive<{
errors: Record<string, boolean>
sandboxAttributes: (devUrl?: string | null) => string
}>({
errors: {},
sandboxAttributes: (devUrl) => {
return devUrl
? [...sandboxDefault, 'allow-same-origin'].join(' ')
: sandboxDefault.join(' ')
},
})
const { platform } = useDeviceStore()
// Generate extension URL (uses cached platform)
const getExtensionUrl = (
publicKey: string,
name: string,
version: string,
assetPath: string = 'index.html',
devServerUrl?: string,
) => {
if (!publicKey || !name || !version) {
console.error('Missing required extension fields')
return ''
}
// If dev server URL is provided, load directly from dev server
if (devServerUrl) {
const cleanUrl = devServerUrl.replace(/\/$/, '')
const cleanPath = assetPath.replace(/^\//, '')
return cleanPath ? `${cleanUrl}/${cleanPath}` : cleanUrl
}
const extensionInfo = {
name,
publicKey,
version,
}
const encodedInfo = btoa(JSON.stringify(extensionInfo))
if (platform === 'android' || platform === 'windows') {
// Android: Tauri uses http://{scheme}.localhost format
return `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/${assetPath}`
} else {
// Desktop: Use custom protocol with base64 as host
return `${EXTENSION_PROTOCOL_PREFIX}${encodedInfo}/${assetPath}`
}
}
// Console logging - use global logs from plugin
const { $consoleLogs, $clearConsoleLogs } = useNuxtApp()
const showConsole = ref(false)
@ -233,7 +329,10 @@ const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
// Registriere IFrame im globalen Message Handler Registry
const tab = tabsStore.openTabs.get(extensionId)
if (tab?.extension) {
console.log('[Vue Debug] Registering iframe in message handler for:', tab.extension.name)
console.log(
'[Vue Debug] Registering iframe in message handler for:',
tab.extension.name,
)
registerExtensionIFrame(el, tab.extension)
console.log('[Vue Debug] Registration complete!')
} else {
@ -289,44 +388,6 @@ watch(
},
{ deep: true },
)
const os = await platform()
// Extension URL generieren
const getExtensionUrl = (extension: IHaexHubExtension) => {
// Extract key_hash from full_extension_id (everything before first underscore)
const firstUnderscoreIndex = extension.id.indexOf('_')
if (firstUnderscoreIndex === -1) {
console.error('Invalid full_extension_id format:', extension.id)
return ''
}
const keyHash = extension.id.substring(0, firstUnderscoreIndex)
const info = {
key_hash: keyHash,
name: extension.name,
version: extension.version,
}
const jsonString = JSON.stringify(info)
const bytes = new TextEncoder().encode(jsonString)
const encodedInfo = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
// 'android', 'ios', 'windows' etc.
let schemeUrl: string
if (os === 'android' || os === 'windows') {
// Android/Windows: http://<scheme>.localhost/path
schemeUrl = `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/index.html`
} else {
// macOS/Linux/iOS: Klassisch scheme://localhost/path
schemeUrl = `${EXTENSION_PROTOCOL_PREFIX}localhost/${encodedInfo}/index.html`
}
return schemeUrl
}
// Context Changes an alle Tabs broadcasten
const { currentTheme } = storeToRefs(useUiStore())
@ -361,11 +422,38 @@ const copyToClipboard = async (text: string) => {
console.error('Failed to copy:', err)
}
}
// Handle iframe errors (e.g., dev server not running)
const onIFrameError = (extensionId: string) => {
iframe.errors[extensionId] = true
}
// Retry loading iframe (clears error and reloads)
const retryLoadIFrame = (extensionId: string) => {
iframe.errors[extensionId] = false
// Reload the iframe by updating the tab
const tab = tabsStore.openTabs.get(extensionId)
if (tab?.iframe) {
tab.iframe.src = tab.iframe.src // Force reload
}
}
</script>
<i18n lang="yaml">
de:
loading: Erweiterung wird geladen
devServer:
notReachable:
title: Dev-Server nicht erreichbar
description: Der Dev-Server unter {url} ist nicht erreichbar.
howToStart: 'So starten Sie den Dev-Server:'
retry: Erneut versuchen
en:
loading: Extension is loading
devServer:
notReachable:
title: Dev Server Not Reachable
description: The dev server at {url} is not reachable.
howToStart: 'To start the dev server:'
retry: Retry
</i18n>

View File

@ -72,7 +72,6 @@
v-for="ext in filteredExtensions"
:key="ext.id"
:extension="ext"
:is-installed="ext.isInstalled"
@install="onInstallFromMarketplace(ext)"
@details="onShowExtensionDetails(ext)"
/>
@ -203,6 +202,7 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
name: 'HaexPassDummy',
version: '1.0.0',
author: 'HaexHub Team',
public_key: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
description:
'Sicherer Passwort-Manager mit Ende-zu-Ende-Verschlüsselung und Autofill-Funktion.',
icon: 'i-heroicons-lock-closed',
@ -220,6 +220,7 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
name: 'HaexNotes',
version: '2.1.0',
author: 'HaexHub Team',
public_key: 'b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3',
description:
'Markdown-basierter Notizen-Editor mit Syntax-Highlighting und Live-Preview.',
icon: 'i-heroicons-document-text',
@ -237,6 +238,7 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
name: 'HaexBackup',
version: '1.5.2',
author: 'Community',
public_key: 'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4',
description:
'Automatische Backups deiner Daten mit Cloud-Sync-Unterstützung.',
icon: 'i-heroicons-cloud-arrow-up',
@ -254,6 +256,7 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
name: 'HaexCalendar',
version: '3.0.1',
author: 'HaexHub Team',
public_key: 'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5',
description:
'Integrierter Kalender mit Event-Management und Synchronisation.',
icon: 'i-heroicons-calendar',
@ -271,6 +274,7 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
name: 'Haex2FA',
version: '1.2.0',
author: 'Security Team',
public_key: 'e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6',
description:
'2-Faktor-Authentifizierung Manager mit TOTP und Backup-Codes.',
icon: 'i-heroicons-shield-check',
@ -288,6 +292,7 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
name: 'GitHub Integration',
version: '1.0.5',
author: 'Community',
public_key: 'f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7',
description:
'Direkter Zugriff auf GitHub Repositories, Issues und Pull Requests.',
icon: 'i-heroicons-code-bracket',
@ -304,13 +309,27 @@ const marketplaceExtensions = ref<IMarketplaceExtension[]>([
// Mark marketplace extensions as installed if they exist in availableExtensions
const allExtensions = computed((): IMarketplaceExtension[] => {
return marketplaceExtensions.value.map((ext) => ({
...ext,
// Check if this marketplace extension is already installed
isInstalled: extensionStore.availableExtensions.some(
(installed) => installed.name === ext.name,
),
}))
return marketplaceExtensions.value.map((ext) => {
// Extensions are uniquely identified by public_key + name
const installedExt = extensionStore.availableExtensions.find((installed) => {
return installed.publicKey === ext.publicKey && installed.name === ext.name
})
if (installedExt) {
return {
...ext,
isInstalled: true,
// Show installed version if it differs from marketplace version
installedVersion: installedExt.version !== ext.version ? installedExt.version : undefined,
}
}
return {
...ext,
isInstalled: false,
installedVersion: undefined,
}
})
})
// Filtered Extensions
@ -349,12 +368,13 @@ const onSelectExtensionAsync = async () => {
preview.value = await extensionStore.previewManifestAsync(extension.path)
if (!preview.value) return
if (!preview.value?.manifest) return
// Check if already installed using full_extension_id
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
// Check if already installed using public_key + name
const isAlreadyInstalled = extensionStore.availableExtensions.some(
(ext) => ext.id === fullExtensionId,
(ext) =>
ext.publicKey === preview.value!.manifest.public_key &&
ext.name === preview.value!.manifest.name
)
if (isAlreadyInstalled) {
@ -403,13 +423,23 @@ const addExtensionAsync = async () => {
const reinstallExtensionAsync = async () => {
try {
if (!preview.value) return
if (!preview.value?.manifest) return
// Calculate full_extension_id
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
// Find the installed extension to get its current version
const installedExt = extensionStore.availableExtensions.find(
(ext) =>
ext.publicKey === preview.value!.manifest.public_key &&
ext.name === preview.value!.manifest.name
)
// Remove old extension first
await extensionStore.removeExtensionByFullIdAsync(fullExtensionId)
if (installedExt) {
// Remove old extension first
await extensionStore.removeExtensionAsync(
installedExt.publicKey,
installedExt.name,
installedExt.version
)
}
// Then install new version
await addExtensionAsync()
@ -440,7 +470,7 @@ onMounted(async () => {
} */
const removeExtensionAsync = async () => {
if (!extensionToBeRemoved.value?.id) {
if (!extensionToBeRemoved.value?.publicKey || !extensionToBeRemoved.value?.name || !extensionToBeRemoved.value?.version) {
add({
color: 'error',
description: 'Erweiterung kann nicht gelöscht werden',
@ -449,9 +479,10 @@ const removeExtensionAsync = async () => {
}
try {
// Use removeExtensionByFullIdAsync since ext.id is already the full_extension_id
await extensionStore.removeExtensionByFullIdAsync(
extensionToBeRemoved.value.id,
await extensionStore.removeExtensionAsync(
extensionToBeRemoved.value.publicKey,
extensionToBeRemoved.value.name,
extensionToBeRemoved.value.version,
)
await extensionStore.loadExtensionsAsync()
add({

View File

@ -1,11 +0,0 @@
<template>
<div class="flex-1 p-2">
<NuxtPage />
</div>
</template>
<script setup lang="ts">
definePageMeta({
name: 'passwords',
})
</script>

View File

@ -1,108 +0,0 @@
<template>
<div class="">
<HaexPassGroup
v-model="group"
mode="create"
@close="onClose"
@submit="createAsync"
/>
<HaexPassMenuBottom
show-close-button
show-save-button
:has-changes
@close="onClose"
@save="createAsync"
/>
<HaexPassDialogUnsavedChanges
v-model:ignore-changes="ignoreChanges"
v-model:open="showUnsavedChangesDialog"
:has-changes
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
/>
</div>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordGroupCreate',
})
const { currentGroupId } = storeToRefs(usePasswordGroupStore())
const group = ref<SelectHaexPasswordsGroups>({
name: '',
description: '',
id: '',
color: null,
icon: null,
order: null,
parentId: currentGroupId.value || null,
createdAt: null,
updateAt: null,
haex_tombstone: null,
})
const errors = ref({
name: [],
description: [],
})
const ignoreChanges = ref(false)
const onClose = () => {
if (showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value) {
return (showUnsavedChangesDialog.value = true)
}
useRouter().back()
}
const { addGroupAsync } = usePasswordGroupStore()
const createAsync = async () => {
try {
if (errors.value.name.length || errors.value.description.length) return
const newGroup = await addGroupAsync(group.value)
if (!newGroup.id) {
return
}
ignoreChanges.value = true
await navigateTo(
useLocalePath()({
name: 'passwordGroupItems',
params: {
groupId: newGroup.id,
},
query: {
...useRoute().query,
},
}),
)
} catch (error) {
console.log(error)
}
}
const hasChanges = computed(() => {
return !!(
group.value.color ||
group.value.description ||
group.value.icon ||
group.value.name
)
})
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
ignoreChanges.value = true
onClose()
}
</script>

View File

@ -1,177 +0,0 @@
<template>
<div>
<HaexPassGroup
v-model="group"
:read-only
mode="edit"
@close="onClose"
@submit="onSaveAsync"
/>
<HaexPassMenuBottom
:show-edit-button="readOnly && !hasChanges"
:show-readonly-button="!readOnly && !hasChanges"
:show-save-button="hasChanges"
:has-changes
show-close-button
show-delete-button
@close="onClose()"
@delete="showConfirmDeleteDialog = true"
@edit="readOnly = false"
@readonly="readOnly = true"
@save="onSaveAsync"
/>
<HaexPassDialogDeleteItem
v-model:open="showConfirmDeleteDialog"
:item-name="group.name"
:final="inTrashGroup"
@abort="showConfirmDeleteDialog = false"
@confirm="onDeleteAsync"
/>
<HaexPassDialogUnsavedChanges
v-model:ignore-changes="ignoreChanges"
v-model:open="showUnsavedChangesDialog"
:has-changes="hasChanges"
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
/>
</div>
</template>
<script setup lang="ts">
import type { SelectHaexPasswordsGroups } from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordGroupEdit',
})
const { t } = useI18n()
const { inTrashGroup, currentGroupId } = storeToRefs(usePasswordGroupStore())
const group = ref<SelectHaexPasswordsGroups>({
color: null,
createdAt: null,
description: null,
icon: null,
id: '',
name: '',
order: null,
parentId: null,
updateAt: null,
haex_tombstone: null,
})
const original = ref<string>('')
const ignoreChanges = ref(false)
const { readGroupAsync } = usePasswordGroupStore()
watchImmediate(currentGroupId, async () => {
if (!currentGroupId.value) return
ignoreChanges.value = false
try {
const foundGroup = await readGroupAsync(currentGroupId.value)
if (foundGroup) {
original.value = JSON.parse(JSON.stringify(foundGroup))
group.value = foundGroup
}
} catch (error) {
console.error(error)
}
})
const hasChanges = computed(() => {
const current = JSON.stringify(group.value)
const origin = JSON.stringify(original.value)
console.log('hasChanges', current, origin)
return !(current === origin)
})
const readOnly = ref(false)
const onClose = () => {
if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return
readOnly.value = true
useRouter().back()
}
const { add } = useToast()
const { updateAsync, syncGroupItemsAsync, deleteGroupAsync } =
usePasswordGroupStore()
const onSaveAsync = async () => {
try {
if (!group.value) return
ignoreChanges.value = true
await updateAsync(group.value)
await syncGroupItemsAsync()
add({ color: 'success', description: t('change.success') })
onClose()
} catch (error) {
add({ color: 'error', description: t('change.error') })
console.log(error)
}
}
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
onClose()
}
const showConfirmDeleteDialog = ref(false)
const onDeleteAsync = async () => {
try {
const parentId = group.value.parentId
await deleteGroupAsync(group.value.id, inTrashGroup.value)
await syncGroupItemsAsync()
showConfirmDeleteDialog.value = false
ignoreChanges.value = true
await navigateTo(
useLocalePath()({
name: 'passwordGroupItems',
params: {
...useRouter().currentRoute.value.params,
groupId: parentId,
},
}),
)
} catch (error) {
console.error(error)
}
}
</script>
<i18n lang="yaml">
de:
title: Gruppe ändern
abort: Abbrechen
save: Speichern
name:
label: Name
description:
label: Beschreibung
change:
success: Änderung erfolgreich gespeichert
error: Änderung konnte nicht gespeichert werden
en:
title: Edit Group
abort: Abort
save: Save
name:
label: Name
description:
label: Description
change:
success: Change successfully saved
error: Change could not be saved
</i18n>

View File

@ -1,273 +0,0 @@
<template>
<div class="flex flex-1">
<!-- <div class="h-screen bg-accented">aaa</div> -->
<div class="flex flex-col flex-1">
<HaexPassGroupBreadcrumbs
v-show="breadCrumbs.length"
:items="breadCrumbs"
class="px-2 sticky -top-2 z-10"
/>
<!-- <div class="flex-1 py-1 flex"> -->
<HaexPassMobileMenu
ref="listRef"
v-model:selected-items="selectedItems"
:menu-items="groupItems"
/>
<!-- </div> -->
<div
class="fixed bottom-16 flex justify-between transition-all w-full sm:items-center items-end px-8 z-40"
>
<div class="w-full" />
<UDropdownMenu
v-model:open="open"
:items="menu"
>
<UButton
icon="mdi:plus"
:ui="{
base: 'rotate-45 z-40',
leadingIcon: [open ? 'rotate-0' : 'rotate-45', 'transition-all'],
}"
size="xl"
/>
</UDropdownMenu>
<div
class="flex flex-col sm:flex-row gap-4 w-full justify-end items-end"
>
<UiButton
v-show="selectedItems.size === 1"
color="secondary"
icon="mdi:pencil"
:tooltip="t('edit')"
@click="onEditAsync"
/>
<UiButton
v-show="selectedItems.size"
color="secondary"
:tooltip="t('cut')"
icon="mdi:scissors"
@click="onCut"
/>
<UiButton
v-show="selectedGroupItems?.length"
color="secondary"
icon="proicons:clipboard-paste"
:tooltip="t('paste')"
@click="onPasteAsync"
/>
<UiButton
v-show="selectedItems.size"
color="secondary"
icon="mdi:trash-outline"
:tooltip="t('delete')"
@click="onDeleteAsync"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { IPasswordMenuItem } from '~/components/haex/pass/mobile/menu/types'
//import { useMagicKeys, whenever } from '@vueuse/core'
import Fuse from 'fuse.js'
definePageMeta({
name: 'passwordGroupItems',
})
const open = ref(false)
const { t } = useI18n()
const { add } = useToast()
const selectedItems = ref<Set<IPasswordMenuItem>>(new Set())
const { menu } = storeToRefs(usePasswordsActionMenuStore())
const { syncItemsAsync } = usePasswordItemStore()
const { syncGroupItemsAsync } = usePasswordGroupStore()
onMounted(async () => {
try {
await Promise.allSettled([syncItemsAsync(), syncGroupItemsAsync()])
} catch (error) {
console.error(error)
}
})
const {
breadCrumbs,
currentGroupId,
inTrashGroup,
selectedGroupItems,
groups,
} = storeToRefs(usePasswordGroupStore())
const { items } = storeToRefs(usePasswordItemStore())
const { search } = storeToRefs(useSearchStore())
const groupItems = computed<IPasswordMenuItem[]>(() => {
const menuItems: IPasswordMenuItem[] = []
const filteredGroups = search.value
? new Fuse(groups.value, {
keys: ['name', 'description'],
findAllMatches: true,
})
.search(search.value)
.map((match) => match.item)
: groups.value.filter((group) => group.parentId == currentGroupId.value)
const filteredItems = search.value
? new Fuse(items.value, {
keys: ['title', 'note', 'password', 'tags', 'url', 'username'],
})
.search(search.value)
.map((match) => match.item)
: items.value.filter(
(item) =>
item.haex_passwords_group_items.groupId == currentGroupId.value,
)
menuItems.push(
...filteredGroups.map<IPasswordMenuItem>((group) => ({
color: group.color,
icon: group.icon,
id: group.id,
name: group.name,
type: 'group',
})),
)
menuItems.push(
...filteredItems.map<IPasswordMenuItem>((item) => ({
icon: item.haex_passwords_item_details.icon,
id: item.haex_passwords_item_details.id,
name: item.haex_passwords_item_details.title,
type: 'item',
})),
)
return menuItems
})
const onEditAsync = async () => {
const item = selectedItems.value.values().next().value
if (item?.type === 'group')
await navigateTo(
useLocalePath()({
name: 'passwordGroupEdit',
params: { groupId: item.id },
}),
)
else if (item?.type === 'item') {
await navigateTo(
useLocalePath()({
name: 'passwordItemEdit',
params: { itemId: item.id },
}),
)
}
}
onKeyStroke('e', async (e) => {
if (e.ctrlKey) {
await onEditAsync()
}
})
const onCut = () => {
selectedGroupItems.value = [...selectedItems.value]
selectedItems.value.clear()
}
onKeyStroke('x', (event) => {
if (event.ctrlKey && selectedItems.value.size) {
event.preventDefault()
onCut()
}
})
const { insertGroupItemsAsync } = usePasswordGroupStore()
const onPasteAsync = async () => {
if (!selectedGroupItems.value?.length) return
try {
await insertGroupItemsAsync(
[...selectedGroupItems.value],
currentGroupId.value,
)
await syncGroupItemsAsync()
selectedGroupItems.value = []
selectedItems.value.clear()
} catch (error) {
console.error(error)
selectedGroupItems.value = []
add({ color: 'error', description: t('error.paste') })
}
}
onKeyStroke('v', async (event) => {
if (event.ctrlKey) {
await onPasteAsync()
}
})
/* const { escape } = useMagicKeys()
whenever(escape, () => {
selectedItems.value.clear()
}) */
onKeyStroke('escape', () => selectedItems.value.clear())
onKeyStroke('a', (event) => {
if (event.ctrlKey) {
event.preventDefault()
event.stopImmediatePropagation()
selectedItems.value = new Set(groupItems.value)
}
})
const { deleteAsync } = usePasswordItemStore()
const { deleteGroupAsync } = usePasswordGroupStore()
const onDeleteAsync = async () => {
for (const item of selectedItems.value) {
if (item.type === 'group') {
await deleteGroupAsync(item.id, inTrashGroup.value)
}
if (item.type === 'item') {
await deleteAsync(item.id, inTrashGroup.value)
}
}
selectedItems.value.clear()
await syncGroupItemsAsync()
}
/* const keys = useMagicKeys()
whenever(keys, async () => {
await onDeleteAsync()
}) */
onKeyStroke('delete', () => onDeleteAsync())
const listRef = useTemplateRef<HTMLElement>('listRef')
onClickOutside(listRef, () => setTimeout(() => selectedItems.value.clear(), 50))
</script>
<i18n lang="yaml">
de:
cut: Ausschneiden
paste: Einfügen
delete: Löschen
edit: Bearbeiten
wtf: 'wtf'
en:
cut: Cut
paste: Paste
delete: Delete
edit: Edit
</i18n>

View File

@ -1,226 +0,0 @@
<template>
<div>
<!-- <div class="flex flex-col">
<p>
{{ item.originalDetails }}
</p>
{{ item.details }}
</div> -->
<HaexPassItem
v-model:details="item.details"
v-model:key-values-add="item.keyValuesAdd"
v-model:key-values-delete="item.keyValuesDelete"
v-model:key-values="item.keyValues"
:history="item.history"
:read-only
@close="onClose()"
@submit="onUpdateAsync"
/>
<HaexPassMenuBottom
:has-changes
:show-edit-button="readOnly && !hasChanges"
:show-readonly-button="!readOnly && !hasChanges"
:show-save-button="!readOnly && hasChanges"
show-close-button
show-delete-button
@close="onClose"
@delete="showConfirmDeleteDialog = true"
@edit="readOnly = false"
@readonly="readOnly = true"
@save="onUpdateAsync"
/>
<HaexPassDialogDeleteItem
v-model:open="showConfirmDeleteDialog"
@abort="showConfirmDeleteDialog = false"
@confirm="deleteItemAsync"
/>
<HaexPassDialogUnsavedChanges
v-model:ignore-changes="ignoreChanges"
v-model:open="showUnsavedChangesDialog"
:has-changes="hasChanges"
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
/>
</div>
</template>
<script setup lang="ts">
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
SelectHaexPasswordsItemKeyValues,
} from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordItemEdit',
})
/* defineProps({
icon: String,
title: String,
withCopyButton: Boolean,
}) */
const readOnly = ref(true)
const showConfirmDeleteDialog = ref(false)
const { t } = useI18n()
const item = reactive<{
details: SelectHaexPasswordsItemDetails
history: SelectHaexPasswordsItemHistory[]
keyValues: SelectHaexPasswordsItemKeyValues[]
keyValuesAdd: SelectHaexPasswordsItemKeyValues[]
keyValuesDelete: SelectHaexPasswordsItemKeyValues[]
originalDetails: SelectHaexPasswordsItemDetails | null
originalKeyValues: SelectHaexPasswordsItemKeyValues[] | null
}>({
details: {
id: '',
createdAt: null,
icon: null,
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
haex_tombstone: null,
},
keyValues: [],
history: [],
keyValuesAdd: [],
keyValuesDelete: [],
originalDetails: {
id: '',
createdAt: null,
icon: null,
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
haex_tombstone: null,
},
originalKeyValues: null,
})
const { currentItem } = storeToRefs(usePasswordItemStore())
watch(
currentItem,
() => {
if (!currentItem.value) return
item.details = JSON.parse(JSON.stringify(currentItem.value?.details))
item.keyValues = JSON.parse(JSON.stringify(currentItem.value?.keyValues))
item.history = JSON.parse(JSON.stringify(currentItem.value?.history))
item.keyValuesAdd = []
item.keyValuesDelete = []
item.originalDetails = JSON.parse(
JSON.stringify(currentItem.value?.details),
)
item.originalKeyValues = JSON.parse(
JSON.stringify(currentItem.value?.keyValues),
)
},
{ immediate: true },
)
const { add } = useToast()
const { deleteAsync, updateAsync } = usePasswordItemStore()
const { syncGroupItemsAsync } = usePasswordGroupStore()
const { currentGroupId, inTrashGroup } = storeToRefs(usePasswordGroupStore())
const ignoreChanges = ref(false)
const onUpdateAsync = async () => {
try {
const newId = await updateAsync({
details: item.details,
groupId: currentGroupId.value || null,
keyValues: item.keyValues,
keyValuesAdd: item.keyValuesAdd,
keyValuesDelete: item.keyValuesDelete,
})
if (newId) add({ color: 'success', description: t('success.update') })
syncGroupItemsAsync()
ignoreChanges.value = true
onClose()
} catch (error) {
console.log(error)
add({ color: 'error', description: t('error.update') })
}
}
const onClose = () => {
if (showConfirmDeleteDialog.value || showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value)
return (showUnsavedChangesDialog.value = true)
readOnly.value = true
useRouter().back()
}
const deleteItemAsync = async () => {
try {
await deleteAsync(item.details.id, inTrashGroup.value)
showConfirmDeleteDialog.value = false
add({ color: 'success', description: t('success.delete') })
await syncGroupItemsAsync()
onClose()
} catch (errro) {
console.log(errro)
add({
color: 'error',
description: t('error.delete'),
})
}
}
const hasChanges = computed(() => {
return !(
JSON.stringify(item.originalDetails) === JSON.stringify(item.details) &&
JSON.stringify(item.originalKeyValues) === JSON.stringify(item.keyValues) &&
!item.keyValuesAdd.length &&
!item.keyValuesDelete.length
)
})
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
ignoreChanges.value = true
onClose()
}
</script>
<i18n lang="yaml">
de:
success:
update: Eintrag erfolgreich aktualisiert
delete: Eintrag wurde gelöscht
error:
update: Eintrag konnte nicht aktualisiert werden
delete: Eintrag konnte nicht gelöscht werden
tab:
details: Details
keyValue: Extra
history: Verlauf
en:
success:
update: Entry successfully updated
delete: Entry successfully removed
error:
update: Entry could not be updated
delete: Entry could not be deleted
tab:
details: Details
keyValue: Extra
history: History
</i18n>

View File

@ -1,159 +0,0 @@
<template>
<div>
<HaexPassItem
v-model:details="item.details"
v-model:key-values-add="item.keyValuesAdd"
:default-icon="currentGroup?.icon"
:history="item.history"
@close="onClose"
@submit="onCreateAsync"
/>
<HaexPassMenuBottom
:has-changes
:show-close-button="true"
:show-save-button="true"
@close="onClose"
@save="onCreateAsync"
/>
<HaexPassDialogUnsavedChanges
v-model:ignore-changes="ignoreChanges"
v-model:open="showUnsavedChangesDialog"
:has-changes="hasChanges"
@abort="showUnsavedChangesDialog = false"
@confirm="onConfirmIgnoreChanges"
/>
</div>
</template>
<script setup lang="ts">
import type {
SelectHaexPasswordsItemDetails,
SelectHaexPasswordsItemHistory,
SelectHaexPasswordsItemKeyValues,
} from '~~/src-tauri/database/schemas/vault'
definePageMeta({
name: 'passwordItemCreate',
})
defineProps<{
icon: string
title: string
withCopyButton: boolean
}>()
const { t } = useI18n()
const item = reactive<{
details: SelectHaexPasswordsItemDetails
history: SelectHaexPasswordsItemHistory[]
keyValuesAdd: SelectHaexPasswordsItemKeyValues[]
originalDetails: SelectHaexPasswordsItemDetails
originalKeyValuesAdd: []
}>({
details: {
createdAt: null,
haex_tombstone: null,
icon: null,
id: '',
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
},
history: [],
keyValuesAdd: [],
originalDetails: {
createdAt: null,
haex_tombstone: null,
icon: null,
id: '',
note: null,
password: null,
tags: null,
title: null,
updateAt: null,
url: null,
username: null,
},
originalKeyValuesAdd: [],
})
const { add } = useToast()
const { currentGroup } = storeToRefs(usePasswordGroupStore())
const { syncGroupItemsAsync } = usePasswordGroupStore()
const { addAsync } = usePasswordItemStore()
const onCreateAsync = async () => {
try {
const newId = await addAsync(
item.details,
item.keyValuesAdd,
currentGroup.value,
)
if (newId) {
ignoreChanges.value = true
add({ color: 'success', description: t('success') })
await syncGroupItemsAsync()
onClose()
}
} catch (error) {
console.log(error)
add({ color: 'error', description: t('error') })
}
}
const ignoreChanges = ref(false)
const onClose = () => {
if (showUnsavedChangesDialog.value) return
if (hasChanges.value && !ignoreChanges.value)
return (showUnsavedChangesDialog.value = true)
useRouter().back()
}
const { areItemsEqual } = usePasswordGroup()
const hasChanges = computed(
() =>
!!(
!areItemsEqual(item.originalDetails, item.details) ||
item.keyValuesAdd.length
),
)
const showUnsavedChangesDialog = ref(false)
const onConfirmIgnoreChanges = () => {
showUnsavedChangesDialog.value = false
ignoreChanges.value = true
onClose()
}
</script>
<i18n lang="yaml">
de:
create: Anlegen
abort: Abbrechen
success: Eintrag erfolgreich erstellt
error: Eintrag konnte nicht erstellt werden
tab:
details: Details
keyValue: Extra
history: Verlauf
en:
create: Create
abort: Abort
success: Entry successfully created
error: Entry could not be created
tab:
details: Details
keyValue: Extra
history: History
</i18n>

View File

@ -1,38 +1,43 @@
<template>
<div
class="grid grid-rows-2 sm:grid-cols-2 sm:gap-2 p-2 max-w-2xl w-full h-fit"
>
<div class="p-2">{{ t('language') }}</div>
<div><UiDropdownLocale @select="onSelectLocaleAsync" /></div>
<div>
<div
class="grid grid-rows-2 sm:grid-cols-2 sm:gap-2 p-2 max-w-2xl w-full h-fit"
>
<div class="p-2">{{ t('language') }}</div>
<div><UiDropdownLocale @select="onSelectLocaleAsync" /></div>
<div class="p-2">{{ t('design') }}</div>
<div><UiDropdownTheme @select="onSelectThemeAsync" /></div>
<div class="p-2">{{ t('design') }}</div>
<div><UiDropdownTheme @select="onSelectThemeAsync" /></div>
<div class="p-2">{{ t('vaultName.label') }}</div>
<div>
<UiInput
v-model="currentVaultName"
:placeholder="t('vaultName.label')"
@change="onSetVaultNameAsync"
/>
<div class="p-2">{{ t('vaultName.label') }}</div>
<div>
<UiInput
v-model="currentVaultName"
:placeholder="t('vaultName.label')"
@change="onSetVaultNameAsync"
/>
</div>
<div class="p-2">{{ t('notifications.label') }}</div>
<div>
<UiButton
:label="t('notifications.requestPermission')"
@click="requestNotificationPermissionAsync"
/>
</div>
<div class="p-2">{{ t('deviceName.label') }}</div>
<div>
<UiInput
v-model="deviceName"
:placeholder="t('deviceName.label')"
@change="onUpdateDeviceNameAsync"
/>
</div>
</div>
<div class="p-2">{{ t('notifications.label') }}</div>
<div>
<UiButton
:label="t('notifications.requestPermission')"
@click="requestNotificationPermissionAsync"
/>
</div>
<div class="p-2">{{ t('deviceName.label') }}</div>
<div>
<UiInput
v-model="deviceName"
:placeholder="t('deviceName.label')"
@change="onUpdateDeviceNameAsync"
/>
</div>
<!-- Child routes (like developer.vue) will be rendered here -->
<NuxtPage />
</div>
</template>

View File

@ -0,0 +1,279 @@
<template>
<div class="p-4 max-w-4xl mx-auto space-y-6">
<div class="space-y-2">
<h1 class="text-2xl font-bold">{{ t('title') }}</h1>
<p class="text-sm opacity-70">{{ t('description') }}</p>
</div>
<!-- Add Dev Extension Form -->
<UCard class="p-4 space-y-4">
<h2 class="text-lg font-semibold">{{ t('add.title') }}</h2>
<div class="space-y-2">
<label class="text-sm font-medium">{{ t('add.extensionPath') }}</label>
<div class="flex gap-2">
<UiInput
v-model="extensionPath"
:placeholder="t('add.extensionPathPlaceholder')"
class="flex-1"
/>
<UiButton
:label="t('add.browse')"
variant="outline"
@click="browseExtensionPathAsync"
/>
</div>
<p class="text-xs opacity-60">{{ t('add.extensionPathHint') }}</p>
</div>
<UiButton
:label="t('add.loadExtension')"
:loading="isLoading"
:disabled="!extensionPath"
@click="loadDevExtensionAsync"
/>
</UCard>
<!-- List of Dev Extensions -->
<div
v-if="devExtensions.length > 0"
class="space-y-2"
>
<h2 class="text-lg font-semibold">{{ t('list.title') }}</h2>
<UCard
v-for="ext in devExtensions"
:key="ext.id"
class="p-4 flex items-center justify-between"
>
<div class="space-y-1">
<div class="flex items-center gap-2">
<h3 class="font-medium">{{ ext.name }}</h3>
<UBadge color="info">DEV</UBadge>
</div>
<p class="text-sm opacity-70">v{{ ext.version }}</p>
<p class="text-xs opacity-50">{{ ext.publicKey.slice(0, 16) }}...</p>
</div>
<div class="flex gap-2">
<UiButton
:label="t('list.reload')"
variant="outline"
size="sm"
@click="reloadDevExtensionAsync(ext)"
/>
<UiButton
:label="t('list.remove')"
variant="ghost"
size="sm"
color="error"
@click="removeDevExtensionAsync(ext)"
/>
</div>
</UCard>
</div>
<div
v-else
class="text-center py-8 opacity-50"
>
{{ t('list.empty') }}
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
definePageMeta({
name: 'settings-developer',
})
const { t } = useI18n()
const { add } = useToast()
const { loadExtensionsAsync } = useExtensionsStore()
// State
const extensionPath = ref('')
const isLoading = ref(false)
const devExtensions = ref<
Array<{
id: string
publicKey: string
name: string
version: string
enabled: boolean
}>
>([])
// Load dev extensions on mount
onMounted(async () => {
await loadDevExtensionListAsync()
})
// Browse for extension directory
const browseExtensionPathAsync = async () => {
try {
const selected = await open({
directory: true,
multiple: false,
title: t('add.browseTitle'),
})
if (selected && typeof selected === 'string') {
extensionPath.value = selected
}
} catch (error) {
console.error('Failed to browse directory:', error)
add({
description: t('add.errors.browseFailed'),
color: 'error',
})
}
}
// Load a dev extension
const loadDevExtensionAsync = async () => {
if (!extensionPath.value) return
isLoading.value = true
try {
const extensionId = await invoke<string>('load_dev_extension', {
extensionPath: extensionPath.value,
})
add({
description: t('add.success'),
color: 'success',
})
// Reload list
await loadDevExtensionListAsync()
// Reload all extensions in the main extension store so they appear in the launcher
await loadExtensionsAsync()
// Clear input
extensionPath.value = ''
} catch (error: any) {
console.error('Failed to load dev extension:', error)
add({
description: error || t('add.errors.loadFailed'),
color: 'error',
})
} finally {
isLoading.value = false
}
}
// Load all dev extensions (for the list on this page)
const loadDevExtensionListAsync = async () => {
try {
const extensions = await invoke<Array<any>>('get_all_dev_extensions')
devExtensions.value = extensions
} catch (error) {
console.error('Failed to load dev extensions:', error)
}
}
// Reload a dev extension (removes and re-adds)
const reloadDevExtensionAsync = async (ext: any) => {
try {
// Get the extension path from somewhere (we need to store this)
// For now, just show a message
add({
description: t('list.reloadInfo'),
color: 'info',
})
} catch (error: any) {
console.error('Failed to reload dev extension:', error)
add({
description: error || t('list.errors.reloadFailed'),
color: 'error',
})
}
}
// Remove a dev extension
const removeDevExtensionAsync = async (ext: any) => {
try {
await invoke('remove_dev_extension', {
publicKey: ext.publicKey,
name: ext.name,
})
add({
description: t('list.removeSuccess'),
color: 'success',
})
// Reload list
await loadDevExtensionListAsync()
// Reload all extensions store
await loadExtensionsAsync()
} catch (error: any) {
console.error('Failed to remove dev extension:', error)
add({
description: error || t('list.errors.removeFailed'),
color: 'error',
})
}
}
</script>
<i18n lang="yaml">
de:
title: Entwicklereinstellungen
description: Lade Extensions im Entwicklungsmodus für schnelleres Testen mit Hot-Reload.
add:
title: Dev-Extension hinzufügen
extensionPath: Extension-Pfad
extensionPathPlaceholder: /pfad/zu/deiner/extension
extensionPathHint: Pfad zum Extension-Projekt (enthält haextension/ und haextension.json)
browse: Durchsuchen
browseTitle: Extension-Verzeichnis auswählen
loadExtension: Extension laden
success: Dev-Extension erfolgreich geladen
errors:
browseFailed: Verzeichnis konnte nicht ausgewählt werden
loadFailed: Extension konnte nicht geladen werden
list:
title: Geladene Dev-Extensions
empty: Keine Dev-Extensions geladen
reload: Neu laden
remove: Entfernen
reloadInfo: Extension wird beim nächsten Laden automatisch aktualisiert
removeSuccess: Dev-Extension erfolgreich entfernt
errors:
reloadFailed: Extension konnte nicht neu geladen werden
removeFailed: Extension konnte nicht entfernt werden
en:
title: Developer Settings
description: Load extensions in development mode for faster testing with hot-reload.
add:
title: Add Dev Extension
extensionPath: Extension Path
extensionPathPlaceholder: /path/to/your/extension
extensionPathHint: Path to your extension project (contains haextension/ and haextension.json)
browse: Browse
browseTitle: Select Extension Directory
loadExtension: Load Extension
success: Dev extension loaded successfully
errors:
browseFailed: Failed to select directory
loadFailed: Failed to load extension
list:
title: Loaded Dev Extensions
empty: No dev extensions loaded
reload: Reload
remove: Remove
reloadInfo: Extension will be automatically updated on next load
removeSuccess: Dev extension removed successfully
errors:
reloadFailed: Failed to reload extension
removeFailed: Failed to remove extension
</i18n>