mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
Update to SDK v1.9.10 with centralized method names
- Updated @haexhub/sdk dependency from 1.9.7 to 1.9.10 - Imported HAEXTENSION_METHODS and HAEXTENSION_EVENTS from SDK - Updated all handler files to use new nested method constants - Updated extensionMessageHandler to route using constants - Changed application.open routing in web handler - All method names now use haextension:subject:action schema
This commit is contained in:
@ -86,6 +86,21 @@ const extension = computed(() => {
|
||||
})
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
console.log('[ExtensionFrame] Iframe loaded successfully for:', extension.value?.name)
|
||||
|
||||
// Try to inject a test script to see if JavaScript execution works
|
||||
try {
|
||||
if (iframeRef.value?.contentWindow) {
|
||||
console.log('[ExtensionFrame] Iframe has contentWindow access')
|
||||
// This will fail with sandboxed iframes without allow-same-origin
|
||||
console.log('[ExtensionFrame] Iframe origin:', iframeRef.value.contentWindow.location.href)
|
||||
} else {
|
||||
console.warn('[ExtensionFrame] Iframe contentWindow is null/undefined')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ExtensionFrame] Cannot access iframe content (expected with sandbox):', e)
|
||||
}
|
||||
|
||||
// Delay the fade-in slightly to allow window animation to mostly complete
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
@ -102,13 +117,28 @@ const sandboxAttributes = computed(() => {
|
||||
|
||||
// Generate extension URL
|
||||
const extensionUrl = computed(() => {
|
||||
if (!extension.value) return ''
|
||||
if (!extension.value) {
|
||||
console.log('[ExtensionFrame] No extension found')
|
||||
return ''
|
||||
}
|
||||
|
||||
const { publicKey, name, version, devServerUrl } = extension.value
|
||||
const assetPath = 'index.html'
|
||||
|
||||
console.log('[ExtensionFrame] Generating URL for extension:', {
|
||||
name,
|
||||
publicKey: publicKey?.substring(0, 10) + '...',
|
||||
version,
|
||||
devServerUrl,
|
||||
platform,
|
||||
})
|
||||
|
||||
if (!publicKey || !name || !version) {
|
||||
console.error('Missing required extension fields')
|
||||
console.error('[ExtensionFrame] Missing required extension fields:', {
|
||||
hasPublicKey: !!publicKey,
|
||||
hasName: !!name,
|
||||
hasVersion: !!version,
|
||||
})
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -116,7 +146,9 @@ const extensionUrl = computed(() => {
|
||||
if (devServerUrl) {
|
||||
const cleanUrl = devServerUrl.replace(/\/$/, '')
|
||||
const cleanPath = assetPath.replace(/^\//, '')
|
||||
return cleanPath ? `${cleanUrl}/${cleanPath}` : cleanUrl
|
||||
const url = cleanPath ? `${cleanUrl}/${cleanPath}` : cleanUrl
|
||||
console.log('[ExtensionFrame] Using dev server URL:', url)
|
||||
return url
|
||||
}
|
||||
|
||||
const extensionInfo = {
|
||||
@ -126,13 +158,18 @@ const extensionUrl = computed(() => {
|
||||
}
|
||||
const encodedInfo = btoa(JSON.stringify(extensionInfo))
|
||||
|
||||
let url = ''
|
||||
if (platform === 'android' || platform === 'windows') {
|
||||
// Android: Tauri uses http://{scheme}.localhost format
|
||||
return `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/${assetPath}`
|
||||
url = `http://${EXTENSION_PROTOCOL_NAME}.localhost/${encodedInfo}/${assetPath}`
|
||||
console.log('[ExtensionFrame] Generated Android/Windows URL:', url)
|
||||
} else {
|
||||
// Desktop: Use custom protocol with base64 as host
|
||||
return `${EXTENSION_PROTOCOL_PREFIX}${encodedInfo}/${assetPath}`
|
||||
url = `${EXTENSION_PROTOCOL_PREFIX}${encodedInfo}/${assetPath}`
|
||||
console.log('[ExtensionFrame] Generated Desktop URL:', url)
|
||||
}
|
||||
|
||||
return url
|
||||
})
|
||||
|
||||
const retryLoad = () => {
|
||||
@ -150,19 +187,28 @@ onMounted(() => {
|
||||
// Wait for iframe to be ready
|
||||
if (iframeRef.value && extension.value) {
|
||||
console.log(
|
||||
'[ExtensionFrame] Manually registering iframe on mount',
|
||||
'[ExtensionFrame] Component MOUNTED',
|
||||
extension.value.name,
|
||||
'windowId:',
|
||||
props.windowId,
|
||||
)
|
||||
registerExtensionIFrame(iframeRef.value, extension.value, props.windowId)
|
||||
} else {
|
||||
console.warn('[ExtensionFrame] Component mounted but missing iframe or extension:', {
|
||||
hasIframe: !!iframeRef.value,
|
||||
hasExtension: !!extension.value,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Explicit cleanup before unmount
|
||||
onBeforeUnmount(() => {
|
||||
console.log('[ExtensionFrame] Component UNMOUNTING', {
|
||||
extensionId: props.extensionId,
|
||||
windowId: props.windowId,
|
||||
hasIframe: !!iframeRef.value,
|
||||
})
|
||||
if (iframeRef.value) {
|
||||
console.log('[ExtensionFrame] Unregistering iframe on unmount')
|
||||
unregisterExtensionIFrame(iframeRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
@ -92,130 +92,142 @@
|
||||
:key="window.id"
|
||||
>
|
||||
<!-- Overview Mode: Teleport to window preview -->
|
||||
<Teleport
|
||||
<template
|
||||
v-if="
|
||||
windowManager.showWindowOverview &&
|
||||
overviewWindowState.has(window.id)
|
||||
"
|
||||
:to="`#window-preview-${window.id}`"
|
||||
>
|
||||
<div
|
||||
class="absolute origin-top-left"
|
||||
:style="{
|
||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||
}"
|
||||
>
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
:id="window.id"
|
||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||
v-model:height="overviewWindowState.get(window.id)!.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find(
|
||||
(ext) => ext.id === window.sourceId,
|
||||
)?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) =>
|
||||
windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
<Teleport :to="`#window-preview-${window.id}`">
|
||||
<div
|
||||
class="absolute origin-top-left"
|
||||
:style="{
|
||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||
}"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
:id="window.id"
|
||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||
v-model:height="
|
||||
overviewWindowState.get(window.id)!.height
|
||||
"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find(
|
||||
(ext) => ext.id === window.sourceId,
|
||||
)?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) =>
|
||||
windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(
|
||||
window.id,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</div>
|
||||
</Teleport>
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<!-- Desktop Mode: Render directly in workspace -->
|
||||
<HaexWindow
|
||||
v-else
|
||||
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||
:id="window.id"
|
||||
v-model:x="window.x"
|
||||
v-model:y="window.y"
|
||||
v-model:width="window.width"
|
||||
v-model:height="window.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||
?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
<template v-else>
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
:id="window.id"
|
||||
v-model:x="window.x"
|
||||
v-model:y="window.y"
|
||||
v-model:width="window.width"
|
||||
v-model:height="window.height"
|
||||
:title="window.title"
|
||||
:icon="window.icon"
|
||||
:is-active="windowManager.isWindowActive(window.id)"
|
||||
:source-x="window.sourceX"
|
||||
:source-y="window.sourceY"
|
||||
:source-width="window.sourceWidth"
|
||||
:source-height="window.sourceHeight"
|
||||
:is-opening="window.isOpening"
|
||||
:is-closing="window.isClosing"
|
||||
:warning-level="
|
||||
window.type === 'extension' &&
|
||||
availableExtensions.find(
|
||||
(ext) => ext.id === window.sourceId,
|
||||
)?.devServerUrl
|
||||
? 'warning'
|
||||
: undefined
|
||||
"
|
||||
class="no-swipe"
|
||||
@close="windowManager.closeWindow(window.id)"
|
||||
@minimize="windowManager.minimizeWindow(window.id)"
|
||||
@activate="windowManager.activateWindow(window.id)"
|
||||
@position-changed="
|
||||
(x, y) =>
|
||||
windowManager.updateWindowPosition(window.id, x, y)
|
||||
"
|
||||
@size-changed="
|
||||
(width, height) =>
|
||||
windowManager.updateWindowSize(window.id, width, height)
|
||||
"
|
||||
@drag-start="handleWindowDragStart(window.id)"
|
||||
@drag-end="handleWindowDragEnd"
|
||||
>
|
||||
<!-- System Window: Render Vue Component -->
|
||||
<component
|
||||
:is="getSystemWindowComponent(window.sourceId)"
|
||||
v-if="window.type === 'system'"
|
||||
/>
|
||||
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</UContextMenu>
|
||||
@ -297,7 +309,7 @@ const currentDraggedItem = reactive({
|
||||
})
|
||||
|
||||
// Track mouse position for showing drop target
|
||||
const { x: mouseX, y: mouseY } = useMouse()
|
||||
const { x: mouseX } = useMouse()
|
||||
|
||||
const dropTargetZone = computed(() => {
|
||||
if (!isDragging.value) return null
|
||||
|
||||
167
src/components/haex/drawer/vault/create.vue
Normal file
167
src/components/haex/drawer/vault/create.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<UiDrawer
|
||||
v-model:open="open"
|
||||
:title="t('title')"
|
||||
:description="t('description')"
|
||||
>
|
||||
<UiButton
|
||||
:label="t('button.label')"
|
||||
:ui="{
|
||||
base: 'px-3 py-2',
|
||||
}"
|
||||
icon="mdi:plus"
|
||||
size="xl"
|
||||
variant="outline"
|
||||
block
|
||||
/>
|
||||
|
||||
<template #content>
|
||||
<div class="p-6 flex flex-col min-h-[50vh]">
|
||||
<div class="flex-1 flex items-center justify-center px-4">
|
||||
<UForm
|
||||
:state="vault"
|
||||
class="w-full max-w-md space-y-6"
|
||||
>
|
||||
<UFormField
|
||||
:label="t('vault.label')"
|
||||
name="name"
|
||||
>
|
||||
<UInput
|
||||
v-model="vault.name"
|
||||
icon="mdi:safe"
|
||||
:placeholder="t('vault.placeholder')"
|
||||
autofocus
|
||||
size="xl"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
:label="t('password.label')"
|
||||
name="password"
|
||||
>
|
||||
<UiInput
|
||||
v-model="vault.password"
|
||||
type="password"
|
||||
icon="i-heroicons-key"
|
||||
:placeholder="t('password.placeholder')"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-auto pt-6">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
block
|
||||
size="xl"
|
||||
@click="open = false"
|
||||
>
|
||||
{{ t('cancel') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
block
|
||||
size="xl"
|
||||
@click="onCreateAsync"
|
||||
>
|
||||
{{ t('create') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UiDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vaultSchema } from './schema'
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
})
|
||||
|
||||
const vault = reactive<{
|
||||
name: string
|
||||
password: string
|
||||
type: 'password' | 'text'
|
||||
}>({
|
||||
name: 'HaexVault',
|
||||
password: '',
|
||||
type: 'password',
|
||||
})
|
||||
|
||||
const initVault = () => {
|
||||
vault.name = 'HaexVault'
|
||||
vault.password = ''
|
||||
vault.type = 'password'
|
||||
}
|
||||
|
||||
const { createAsync } = useVaultStore()
|
||||
const { add } = useToast()
|
||||
|
||||
const check = ref(false)
|
||||
|
||||
const onCreateAsync = async () => {
|
||||
check.value = true
|
||||
|
||||
const nameCheck = vaultSchema.name.safeParse(vault.name)
|
||||
const passwordCheck = vaultSchema.password.safeParse(vault.password)
|
||||
|
||||
if (!nameCheck.success || !passwordCheck.success) return
|
||||
|
||||
open.value = false
|
||||
try {
|
||||
if (vault.name && vault.password) {
|
||||
const vaultId = await createAsync({
|
||||
vaultName: vault.name,
|
||||
password: vault.password,
|
||||
})
|
||||
|
||||
if (vaultId) {
|
||||
initVault()
|
||||
await navigateTo(
|
||||
useLocaleRoute()({ name: 'desktop', params: { vaultId } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
button:
|
||||
label: Vault erstellen
|
||||
vault:
|
||||
label: Vaultname
|
||||
placeholder: Vaultname
|
||||
password:
|
||||
label: Passwort
|
||||
placeholder: Passwort eingeben
|
||||
title: Neue HaexVault erstellen
|
||||
create: Erstellen
|
||||
cancel: Abbrechen
|
||||
description: Erstelle eine neue Vault für deine Daten
|
||||
|
||||
en:
|
||||
button:
|
||||
label: Create vault
|
||||
vault:
|
||||
label: Vault name
|
||||
placeholder: Vault name
|
||||
password:
|
||||
label: Password
|
||||
placeholder: Enter password
|
||||
title: Create new HaexVault
|
||||
create: Create
|
||||
cancel: Cancel
|
||||
description: Create a new vault for your data
|
||||
</i18n>
|
||||
@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<UiDialogConfirm
|
||||
<UiDrawer
|
||||
v-model:open="open"
|
||||
:confirm-label="t('open')"
|
||||
:description="vault.path || path"
|
||||
@confirm="onOpenDatabase"
|
||||
:title="t('title')"
|
||||
:description="path || t('description')"
|
||||
>
|
||||
<UiButton
|
||||
:label="t('vault.open')"
|
||||
:label="t('button.label')"
|
||||
:ui="{
|
||||
base: 'px-3 py-2',
|
||||
}"
|
||||
@ -16,37 +15,63 @@
|
||||
block
|
||||
/>
|
||||
|
||||
<template #title>
|
||||
<i18n-t
|
||||
keypath="title"
|
||||
tag="p"
|
||||
class="flex gap-x-2 text-wrap"
|
||||
>
|
||||
<template #haexvault>
|
||||
<UiTextGradient>HaexVault</UiTextGradient>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-6 flex flex-col min-h-[50vh]">
|
||||
<div class="flex-1 flex items-center justify-center px-4">
|
||||
<div class="w-full max-w-md space-y-4">
|
||||
<div
|
||||
v-if="path"
|
||||
class="text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span class="font-medium">{{ t('path.label') }}:</span>
|
||||
{{ path }}
|
||||
</div>
|
||||
|
||||
<template #body>
|
||||
<UForm
|
||||
:state="vault"
|
||||
class="flex flex-col gap-4 w-full h-full justify-center"
|
||||
>
|
||||
<UiInputPassword
|
||||
v-model="vault.password"
|
||||
class="w-full"
|
||||
autofocus
|
||||
/>
|
||||
<UForm
|
||||
:state="vault"
|
||||
class="w-full"
|
||||
>
|
||||
<UFormField
|
||||
:label="t('password.label')"
|
||||
name="password"
|
||||
>
|
||||
<UInput
|
||||
v-model="vault.password"
|
||||
type="password"
|
||||
icon="i-heroicons-key"
|
||||
:placeholder="t('password.placeholder')"
|
||||
autofocus
|
||||
size="xl"
|
||||
class="w-full"
|
||||
@keyup.enter="onOpenDatabase"
|
||||
/>
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
hidden
|
||||
type="submit"
|
||||
@click="onOpenDatabase"
|
||||
/>
|
||||
</UForm>
|
||||
<div class="flex gap-3 mt-auto pt-6">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
block
|
||||
size="xl"
|
||||
@click="open = false"
|
||||
>
|
||||
{{ t('cancel') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
color="primary"
|
||||
block
|
||||
size="xl"
|
||||
@click="onOpenDatabase"
|
||||
>
|
||||
{{ t('open') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UiDialogConfirm>
|
||||
</UiDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -156,7 +181,12 @@ const onOpenDatabase = async () => {
|
||||
)
|
||||
} catch (error) {
|
||||
open.value = false
|
||||
if (error?.details?.reason === 'file is not a database') {
|
||||
const errorDetails =
|
||||
error && typeof error === 'object' && 'details' in error
|
||||
? (error as { details?: { reason?: string } }).details
|
||||
: undefined
|
||||
|
||||
if (errorDetails?.reason === 'file is not a database') {
|
||||
add({
|
||||
color: 'error',
|
||||
title: t('error.password.title'),
|
||||
@ -171,25 +201,37 @@ const onOpenDatabase = async () => {
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
button:
|
||||
label: Vault öffnen
|
||||
open: Entsperren
|
||||
title: '{haexvault} entsperren'
|
||||
password: Passwort
|
||||
vault:
|
||||
open: Vault öffnen
|
||||
cancel: Abbrechen
|
||||
title: HaexVault entsperren
|
||||
path:
|
||||
label: Pfad
|
||||
password:
|
||||
label: Passwort
|
||||
placeholder: Passwort eingeben
|
||||
description: Öffne eine vorhandene Vault
|
||||
error:
|
||||
open: Vault konnte nicht geöffnet werden
|
||||
password:
|
||||
title: Vault konnte nicht geöffnet werden
|
||||
description: Bitte üperprüfe das Passwort
|
||||
description: Bitte überprüfe das Passwort
|
||||
|
||||
en:
|
||||
button:
|
||||
label: Open Vault
|
||||
open: Unlock
|
||||
title: Unlock {haexvault}
|
||||
password: Passwort
|
||||
cancel: Cancel
|
||||
title: Unlock HaexVault
|
||||
path:
|
||||
label: Path
|
||||
password:
|
||||
label: Password
|
||||
placeholder: Enter password
|
||||
description: Open your existing vault
|
||||
vault:
|
||||
open: Open Vault
|
||||
error:
|
||||
open: Vault couldn't be opened
|
||||
password:
|
||||
title: Vault couldn't be opened
|
||||
description: Please check your password
|
||||
185
src/components/haex/system/debug-logs.vue
Normal file
185
src/components/haex/system/debug-logs.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="w-full h-full bg-default flex flex-col">
|
||||
<!-- Header with controls -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
name="i-heroicons-bug-ant"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
<h2 class="text-lg font-semibold">
|
||||
Debug Logs
|
||||
</h2>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{ logs.length }} logs
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
:label="allCopied ? 'Copied!' : 'Copy All'"
|
||||
:color="allCopied ? 'success' : 'primary'"
|
||||
size="sm"
|
||||
@click="copyAllLogs"
|
||||
/>
|
||||
<UButton
|
||||
label="Clear Logs"
|
||||
color="error"
|
||||
size="sm"
|
||||
@click="clearLogs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Buttons -->
|
||||
<div class="flex gap-2 p-4 border-b border-gray-200 dark:border-gray-700 overflow-x-auto">
|
||||
<UButton
|
||||
v-for="level in ['all', 'log', 'info', 'warn', 'error', 'debug']"
|
||||
:key="level"
|
||||
:label="level"
|
||||
:color="filter === level ? 'primary' : 'neutral'"
|
||||
size="sm"
|
||||
@click="filter = level as any"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Logs Container -->
|
||||
<div
|
||||
ref="logsContainer"
|
||||
class="flex-1 overflow-y-auto p-4 space-y-2 font-mono text-xs"
|
||||
>
|
||||
<div
|
||||
v-for="(log, index) in filteredLogs"
|
||||
:key="index"
|
||||
:class="[
|
||||
'p-3 rounded-lg border-l-4 relative group',
|
||||
log.level === 'error'
|
||||
? 'bg-red-50 dark:bg-red-950/30 border-red-500'
|
||||
: log.level === 'warn'
|
||||
? 'bg-yellow-50 dark:bg-yellow-950/30 border-yellow-500'
|
||||
: log.level === 'info'
|
||||
? 'bg-blue-50 dark:bg-blue-950/30 border-blue-500'
|
||||
: log.level === 'debug'
|
||||
? 'bg-purple-50 dark:bg-purple-950/30 border-purple-500'
|
||||
: 'bg-gray-50 dark:bg-gray-800 border-gray-400',
|
||||
]"
|
||||
>
|
||||
<!-- Copy Button -->
|
||||
<button
|
||||
class="absolute top-2 right-2 p-1.5 rounded bg-white dark:bg-gray-700 shadow-sm hover:bg-gray-100 dark:hover:bg-gray-600 active:scale-95 transition-all"
|
||||
@click="copyLogToClipboard(log)"
|
||||
>
|
||||
<UIcon
|
||||
:name="copiedIndex === index ? 'i-heroicons-check' : 'i-heroicons-clipboard-document'"
|
||||
:class="[
|
||||
'w-4 h-4',
|
||||
copiedIndex === index ? 'text-green-500' : ''
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div class="flex items-start gap-2 mb-1">
|
||||
<span class="text-gray-500 dark:text-gray-400 text-[10px] shrink-0">
|
||||
{{ log.timestamp }}
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
'font-semibold text-[10px] uppercase shrink-0',
|
||||
log.level === 'error'
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: log.level === 'warn'
|
||||
? 'text-yellow-600 dark:text-yellow-400'
|
||||
: log.level === 'info'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: log.level === 'debug'
|
||||
? 'text-purple-600 dark:text-purple-400'
|
||||
: 'text-gray-600 dark:text-gray-400',
|
||||
]"
|
||||
>
|
||||
{{ log.level }}
|
||||
</span>
|
||||
</div>
|
||||
<pre class="whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100 pr-8">{{ log.message }}</pre>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="filteredLogs.length === 0"
|
||||
class="text-center text-gray-500 py-8"
|
||||
>
|
||||
<UIcon
|
||||
name="i-heroicons-document-text"
|
||||
class="w-12 h-12 mx-auto mb-2 opacity-50"
|
||||
/>
|
||||
<p>No logs to display</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { globalConsoleLogs } from '~/plugins/console-interceptor'
|
||||
import type { ConsoleLog } from '~/plugins/console-interceptor'
|
||||
|
||||
const filter = ref<'all' | 'log' | 'info' | 'warn' | 'error' | 'debug'>('all')
|
||||
const logsContainer = ref<HTMLDivElement>()
|
||||
const copiedIndex = ref<number | null>(null)
|
||||
const allCopied = ref(false)
|
||||
|
||||
const { $clearConsoleLogs } = useNuxtApp()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
const logs = computed(() => globalConsoleLogs.value)
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
if (filter.value === 'all') {
|
||||
return logs.value
|
||||
}
|
||||
return logs.value.filter((log) => log.level === filter.value)
|
||||
})
|
||||
|
||||
const clearLogs = () => {
|
||||
if ($clearConsoleLogs) {
|
||||
$clearConsoleLogs()
|
||||
}
|
||||
}
|
||||
|
||||
const copyLogToClipboard = async (log: ConsoleLog) => {
|
||||
const text = `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`
|
||||
await copy(text)
|
||||
|
||||
// Find the index in filteredLogs for visual feedback
|
||||
const index = filteredLogs.value.indexOf(log)
|
||||
copiedIndex.value = index
|
||||
|
||||
// Reset after 2 seconds
|
||||
setTimeout(() => {
|
||||
copiedIndex.value = null
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const copyAllLogs = async () => {
|
||||
const allLogsText = filteredLogs.value
|
||||
.map((log) => `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}`)
|
||||
.join('\n')
|
||||
|
||||
await copy(allLogsText)
|
||||
allCopied.value = true
|
||||
|
||||
// Reset after 2 seconds
|
||||
setTimeout(() => {
|
||||
allCopied.value = false
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive
|
||||
watch(
|
||||
() => logs.value.length,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (logsContainer.value) {
|
||||
logsContainer.value.scrollTop = logsContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<UiDialogConfirm
|
||||
:confirm-label="t('create')"
|
||||
@confirm="onCreateAsync"
|
||||
:description="t('description')"
|
||||
>
|
||||
<UiButton
|
||||
:label="t('vault.create')"
|
||||
:ui="{
|
||||
base: 'px-3 py-2',
|
||||
}"
|
||||
icon="mdi:plus"
|
||||
size="xl"
|
||||
variant="outline"
|
||||
block
|
||||
/>
|
||||
|
||||
<template #title>
|
||||
<i18n-t
|
||||
keypath="title"
|
||||
tag="p"
|
||||
class="flex gap-x-2 flex-wrap"
|
||||
>
|
||||
<template #haexvault>
|
||||
<UiTextGradient>HaexVault</UiTextGradient>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UForm
|
||||
:state="vault"
|
||||
class="flex flex-col gap-4 w-full h-full justify-center"
|
||||
>
|
||||
<UiInput
|
||||
v-model="vault.name"
|
||||
leading-icon="mdi:safe"
|
||||
:label="t('vault.label')"
|
||||
:placeholder="t('vault.placeholder')"
|
||||
/>
|
||||
<UiInputPassword
|
||||
v-model="vault.password"
|
||||
leading-icon="mdi:key-outline"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
hidden
|
||||
type="submit"
|
||||
@click="onCreateAsync"
|
||||
/>
|
||||
</UForm>
|
||||
</template>
|
||||
</UiDialogConfirm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vaultSchema } from './schema'
|
||||
|
||||
const { t } = useI18n({
|
||||
useScope: 'local',
|
||||
})
|
||||
|
||||
const vault = reactive<{
|
||||
name: string
|
||||
password: string
|
||||
type: 'password' | 'text'
|
||||
}>({
|
||||
name: 'HaexVault',
|
||||
password: '',
|
||||
type: 'password',
|
||||
})
|
||||
|
||||
const initVault = () => {
|
||||
vault.name = 'HaexVault'
|
||||
vault.password = ''
|
||||
vault.type = 'password'
|
||||
}
|
||||
|
||||
const { createAsync } = useVaultStore()
|
||||
const { add } = useToast()
|
||||
|
||||
const check = ref(false)
|
||||
const open = ref()
|
||||
|
||||
const onCreateAsync = async () => {
|
||||
check.value = true
|
||||
|
||||
const nameCheck = vaultSchema.name.safeParse(vault.name)
|
||||
const passwordCheck = vaultSchema.password.safeParse(vault.password)
|
||||
|
||||
if (!nameCheck.success || !passwordCheck.success) return
|
||||
|
||||
open.value = false
|
||||
try {
|
||||
if (vault.name && vault.password) {
|
||||
const vaultId = await createAsync({
|
||||
vaultName: vault.name,
|
||||
password: vault.password,
|
||||
})
|
||||
|
||||
if (vaultId) {
|
||||
initVault()
|
||||
await navigateTo(
|
||||
useLocaleRoute()({ name: 'desktop', params: { vaultId } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
vault:
|
||||
create: Neue Vault erstellen
|
||||
label: Vaultname
|
||||
placeholder: Vaultname
|
||||
name: HaexVault
|
||||
title: Neue {haexvault} erstellen
|
||||
create: Erstellen
|
||||
description: Erstelle eine neue Vault für deine Daten
|
||||
|
||||
en:
|
||||
vault:
|
||||
create: Create new vault
|
||||
label: Vaultname
|
||||
placeholder: Vaultname
|
||||
name: HaexVault
|
||||
title: Create new {haexvault}
|
||||
create: Create
|
||||
description: Create a new vault for your data
|
||||
</i18n>
|
||||
@ -5,7 +5,6 @@
|
||||
:readonly="props.readOnly"
|
||||
:leading-icon="props.leadingIcon"
|
||||
:ui="{ base: 'peer' }"
|
||||
size="lg"
|
||||
@change="(e) => $emit('change', e)"
|
||||
@blur="(e) => $emit('blur', e)"
|
||||
@keyup="(e: KeyboardEvent) => $emit('keyup', e)"
|
||||
|
||||
Reference in New Issue
Block a user