mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-19 23:30:51 +01:00
removed haex-pass components
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div class="flex-1 p-2">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
name: 'passwords',
|
||||
})
|
||||
</script>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
||||
279
src/pages/vault/[vaultId]/settings/developer.vue
Normal file
279
src/pages/vault/[vaultId]/settings/developer.vue
Normal 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>
|
||||
Reference in New Issue
Block a user