mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
refactore manifest and permission
This commit is contained in:
@ -4,20 +4,9 @@
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="absolute top-2 right-2">
|
||||
<UiDropdown class="btn btn-sm btn-text btn-circle">
|
||||
<template #activator>
|
||||
<Icon name="mdi:dots-vertical" />
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<UiButton
|
||||
class="btn-error btn-outline btn-sm"
|
||||
@click="showRemoveDialog = true"
|
||||
>
|
||||
<Icon name="mdi:trash" /> {{ t('remove') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiDropdown>
|
||||
<UDropdownMenu>
|
||||
<UiButton icon="mdi:dots-vertical" />
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="card-header">
|
||||
@ -44,6 +33,7 @@
|
||||
<div class="card-actions" v-if="$slots.action">
|
||||
<slot name="action" />
|
||||
</div> -->
|
||||
hier klicken
|
||||
<div
|
||||
class="size-20 absolute bottom-2 right-2"
|
||||
v-html="icon"
|
||||
|
||||
@ -1,71 +1,107 @@
|
||||
<template>
|
||||
<UiAccordion v-if="database?.read?.length">
|
||||
<UAccordion v-if="database?.read?.length">
|
||||
<template #title>
|
||||
<h3>{{ t("permission.read") }}</h3>
|
||||
<h3>{{ t('permission.read') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="read in database?.read" class="flex items-center justify-between px-4 py-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{ Object.keys(read).at(0) }}</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
|
||||
<UiAccordion v-if="database?.write?.length">
|
||||
<template #title>
|
||||
<h3>{{ t("permission.write") }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="write in database?.write" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="read in database?.read"
|
||||
class="flex items-center justify-between px-4 py-1"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(write).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{ Object.keys(write).at(0) }}</label>
|
||||
:id="Object.keys(read).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(read).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(read).at(0)"
|
||||
>{{ Object.keys(read).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
|
||||
<UiAccordion v-if="database?.create?.length">
|
||||
<UAccordion v-if="database?.write?.length">
|
||||
<template #title>
|
||||
<h3>{{ t("permission.create") }}</h3>
|
||||
<h3>{{ t('permission.write') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="create in database?.create" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="write in database?.write"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(create).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(create).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(create).at(0)">{{ Object.keys(create).at(0) }}</label>
|
||||
:id="Object.keys(write).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(write).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(write).at(0)"
|
||||
>{{ Object.keys(write).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
|
||||
<UAccordion v-if="database?.create?.length">
|
||||
<template #title>
|
||||
<h3>{{ t('permission.create') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li
|
||||
v-for="create in database?.create"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(create).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(create).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(create).at(0)"
|
||||
>{{ Object.keys(create).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
defineProps<{ database?: { read?: Record<string, boolean>[], write?: Record<string, boolean>[], create?: Record<string, boolean>[] } }>();
|
||||
const { t } = useI18n();
|
||||
defineProps<{
|
||||
database?: {
|
||||
read?: Record<string, boolean>[]
|
||||
write?: Record<string, boolean>[]
|
||||
create?: Record<string, boolean>[]
|
||||
}
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
create: Erstellen
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
create: Erstellen
|
||||
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
create: Create
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
create: Create
|
||||
</i18n>
|
||||
|
||||
@ -1,38 +1,56 @@
|
||||
<template>
|
||||
<UiAccordion v-if="filesystem?.read?.length">
|
||||
<UAccordion v-if="filesystem?.read?.length">
|
||||
<template #title>
|
||||
<h3>{{ t('permission.read') }}</h3>
|
||||
</template>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="read in filesystem?.read" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="read in filesystem?.read"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input :id="Object.keys(read).at(0)" type="checkbox" class="checkbox" :checked="Object.values(read).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(read).at(0)">{{
|
||||
Object.keys(read).at(0)
|
||||
}}</label>
|
||||
<input
|
||||
:id="Object.keys(read).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(read).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(read).at(0)"
|
||||
>{{ Object.keys(read).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
|
||||
<UiAccordion v-if="filesystem?.write?.length">
|
||||
<UAccordion v-if="filesystem?.write?.length">
|
||||
<template #title>
|
||||
<h3>{{ t('permission.write') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="write in filesystem?.write" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="write in filesystem?.write"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(write).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(write).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(write).at(0)">{{
|
||||
Object.keys(write).at(0)
|
||||
}}</label>
|
||||
:id="Object.keys(write).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(write).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(write).at(0)"
|
||||
>{{ Object.keys(write).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -46,13 +64,13 @@ const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
de:
|
||||
permission:
|
||||
read: Lesen
|
||||
write: Schreiben
|
||||
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
en:
|
||||
permission:
|
||||
read: Read
|
||||
write: Write
|
||||
</i18n>
|
||||
|
||||
@ -1,33 +1,43 @@
|
||||
<template>
|
||||
<UiAccordion>
|
||||
<UAccordion>
|
||||
<template #title>
|
||||
<h3>{{ t("http.access") }}</h3>
|
||||
<h3>{{ t('http.access') }}</h3>
|
||||
</template>
|
||||
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="access in http" class="flex items-center justify-between px-4 py-0.5">
|
||||
<li
|
||||
v-for="access in http"
|
||||
class="flex items-center justify-between px-4 py-0.5"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:id="Object.keys(access).at(0)" type="checkbox" class="checkbox"
|
||||
:checked="Object.values(access).at(0)" >
|
||||
<label class="label-text text-base" :for="Object.keys(access).at(0)">{{ Object.keys(access).at(0) }}</label>
|
||||
:id="Object.keys(access).at(0)"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="Object.values(access).at(0)"
|
||||
/>
|
||||
<label
|
||||
class="label-text text-base"
|
||||
:for="Object.keys(access).at(0)"
|
||||
>{{ Object.keys(access).at(0) }}</label
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</UiAccordion>
|
||||
</UAccordion>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ http?: Record<string, boolean>[] }>();
|
||||
const { t } = useI18n();
|
||||
defineProps<{ http?: Record<string, boolean>[] }>()
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
http:
|
||||
access: Internet Zugriff
|
||||
de:
|
||||
http:
|
||||
access: Internet Zugriff
|
||||
|
||||
en:
|
||||
http:
|
||||
access: Internet Access
|
||||
en:
|
||||
http:
|
||||
access: Internet Access
|
||||
</i18n>
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
/**
|
||||
* Broadcasts context changes to all active extensions
|
||||
*/
|
||||
// composables/extensionContextBroadcast.ts
|
||||
export const useExtensionContextBroadcast = () => {
|
||||
const extensionIframes = ref<HTMLIFrameElement[]>([])
|
||||
// Globaler State für alle aktiven IFrames
|
||||
const extensionIframes = useState<Set<HTMLIFrameElement>>(
|
||||
'extension-iframes',
|
||||
() => new Set(),
|
||||
)
|
||||
|
||||
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value.push(iframe)
|
||||
extensionIframes.value.add(iframe)
|
||||
}
|
||||
|
||||
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe)
|
||||
extensionIframes.value.delete(iframe)
|
||||
}
|
||||
|
||||
const broadcastContextChange = (context: {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { IHaexHubExtensionLink } from '~/types/haexhub'
|
||||
// composables/extensionMessageHandler.ts
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
|
||||
interface ExtensionRequest {
|
||||
id: string
|
||||
@ -7,119 +9,124 @@ interface ExtensionRequest {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
interface ExtensionResponse {
|
||||
id: string
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: string
|
||||
message: string
|
||||
details?: unknown
|
||||
}
|
||||
}
|
||||
// Globaler Handler - nur einmal registriert
|
||||
let globalHandlerRegistered = false
|
||||
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
|
||||
|
||||
export const useExtensionMessageHandler = (
|
||||
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) => {
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
// Security: Only accept messages from our iframe
|
||||
if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) {
|
||||
return
|
||||
const registerGlobalMessageHandler = () => {
|
||||
if (globalHandlerRegistered) return
|
||||
|
||||
window.addEventListener('message', async (event: MessageEvent) => {
|
||||
// Finde die Extension für dieses IFrame
|
||||
let extension: IHaexHubExtension | undefined
|
||||
let sourceIframe: HTMLIFrameElement | undefined
|
||||
|
||||
for (const [iframe, ext] of iframeRegistry.entries()) {
|
||||
if (event.source === iframe.contentWindow) {
|
||||
extension = ext
|
||||
sourceIframe = iframe
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension || !sourceIframe) {
|
||||
return // Message ist nicht von einem registrierten IFrame
|
||||
}
|
||||
|
||||
const request = event.data as ExtensionRequest
|
||||
|
||||
// Validate request structure
|
||||
if (!request.id || !request.method) {
|
||||
console.error('Invalid extension request:', request)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[HaexHub] Extension request:', request.method, request.params)
|
||||
console.log(
|
||||
`[HaexHub] ${extension.name} request:`,
|
||||
request.method,
|
||||
request.params,
|
||||
)
|
||||
|
||||
try {
|
||||
let result: unknown
|
||||
|
||||
// Route request to appropriate handler
|
||||
if (request.method.startsWith('extension.')) {
|
||||
result = await handleExtensionMethod(request, extension)
|
||||
result = await handleExtensionMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('db.')) {
|
||||
result = await handleDatabaseMethod(request, extension)
|
||||
result = await handleDatabaseMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('fs.')) {
|
||||
result = await handleFilesystemMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('http.')) {
|
||||
result = await handleHttpMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('permissions.')) {
|
||||
result = await handlePermissionsMethod(request, extension)
|
||||
result = await handlePermissionsMethodAsync(request, extension)
|
||||
} else if (request.method.startsWith('context.')) {
|
||||
result = await handleContextMethod(request)
|
||||
} else if (request.method.startsWith('search.')) {
|
||||
result = await handleSearchMethod(request, extension)
|
||||
result = await handleContextMethodAsync(request)
|
||||
} else {
|
||||
throw new Error(`Unknown method: ${request.method}`)
|
||||
}
|
||||
|
||||
// Send success response
|
||||
sendResponse(iframeRef.value, {
|
||||
id: request.id,
|
||||
result,
|
||||
})
|
||||
sourceIframe.contentWindow?.postMessage(
|
||||
{
|
||||
id: request.id,
|
||||
result,
|
||||
},
|
||||
'*',
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[HaexHub] Extension request error:', error)
|
||||
|
||||
// Send error response
|
||||
sendResponse(iframeRef.value, {
|
||||
id: request.id,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
details: error,
|
||||
sourceIframe.contentWindow?.postMessage(
|
||||
{
|
||||
id: request.id,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
details: error,
|
||||
},
|
||||
},
|
||||
})
|
||||
'*',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sendResponse = (
|
||||
iframe: HTMLIFrameElement,
|
||||
response: ExtensionResponse,
|
||||
) => {
|
||||
iframe.contentWindow?.postMessage(response, '*')
|
||||
}
|
||||
|
||||
// Register/unregister message listener
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', handleMessage)
|
||||
})
|
||||
|
||||
globalHandlerRegistered = true
|
||||
}
|
||||
|
||||
export const useExtensionMessageHandler = (
|
||||
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
||||
extension: ComputedRef<IHaexHubExtension | undefined | null>,
|
||||
) => {
|
||||
// Registriere globalen Handler beim ersten Aufruf
|
||||
registerGlobalMessageHandler()
|
||||
|
||||
// Registriere dieses IFrame
|
||||
watchEffect(() => {
|
||||
if (iframeRef.value && extension.value) {
|
||||
iframeRegistry.set(iframeRef.value, extension.value)
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup beim Unmount
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
if (iframeRef.value) {
|
||||
iframeRegistry.delete(iframeRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
handleMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Extension Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleExtensionMethod(
|
||||
async function handleExtensionMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'extension.getInfo':
|
||||
return {
|
||||
keyHash: extension.value?.id || '', // TODO: Real key hash
|
||||
name: extension.value?.name || '',
|
||||
fullId: `${extension.value?.id}/${extension.value?.name}@${extension.value?.version}`,
|
||||
version: extension.value?.version || '',
|
||||
displayName: extension.value?.name,
|
||||
namespace: extension.value?.author,
|
||||
allowedOrigin: window.location.origin, // "tauri://localhost"
|
||||
}
|
||||
|
||||
case 'extensions.getDependencies':
|
||||
// TODO: Implement dependencies from manifest
|
||||
return []
|
||||
|
||||
return await invoke('get_extension_info', {
|
||||
extensionId: extension.id,
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unknown extension method: ${request.method}`)
|
||||
}
|
||||
@ -129,47 +136,41 @@ async function handleExtensionMethod(
|
||||
// Database Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleDatabaseMethod(
|
||||
async function handleDatabaseMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
extension: IHaexHubExtension, // Direkter Typ
|
||||
) {
|
||||
const { currentVault } = useVaultStore()
|
||||
if (!currentVault) {
|
||||
throw new Error('No vault available')
|
||||
const params = request.params as {
|
||||
query?: string
|
||||
params?: unknown[]
|
||||
}
|
||||
|
||||
if (!extension.value) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
const params = request.params as { query?: string; params?: unknown[] }
|
||||
|
||||
switch (request.method) {
|
||||
case 'db.query': {
|
||||
// Validate permission
|
||||
await validateDatabaseAccess(extension.value, params.query || '', 'read')
|
||||
|
||||
// Execute query
|
||||
const result = await currentVault.drizzle.execute(params.query || '')
|
||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: result.rows || [],
|
||||
rows,
|
||||
rowsAffected: 0,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
case 'db.execute': {
|
||||
// Validate permission
|
||||
await validateDatabaseAccess(extension.value, params.query || '', 'write')
|
||||
|
||||
// Execute query
|
||||
const result = await currentVault.drizzle.execute(params.query || '')
|
||||
await invoke<string[]>('extension_sql_execute', {
|
||||
sql: params.query || '',
|
||||
params: params.params || [],
|
||||
extensionId: extension.id,
|
||||
})
|
||||
|
||||
return {
|
||||
rows: [],
|
||||
rowsAffected: result.rowsAffected || 0,
|
||||
lastInsertId: result.lastInsertId,
|
||||
rowsAffected: 1,
|
||||
lastInsertId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,18 +178,14 @@ async function handleDatabaseMethod(
|
||||
const statements =
|
||||
(request.params as { statements?: string[] }).statements || []
|
||||
|
||||
// Validate all statements
|
||||
for (const stmt of statements) {
|
||||
await validateDatabaseAccess(extension.value, stmt, 'write')
|
||||
await invoke('extension_sql_execute', {
|
||||
sql: stmt,
|
||||
params: [],
|
||||
extensionId: extension.id,
|
||||
})
|
||||
}
|
||||
|
||||
// Execute transaction
|
||||
await currentVault.drizzle.transaction(async (tx) => {
|
||||
for (const stmt of statements) {
|
||||
await tx.execute(stmt)
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
@ -196,125 +193,63 @@ async function handleDatabaseMethod(
|
||||
throw new Error(`Unknown database method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Validation
|
||||
// Filesystem Methods (TODO)
|
||||
// ==========================================
|
||||
|
||||
async function validateDatabaseAccess(
|
||||
extension: IHaexHubExtensionLink,
|
||||
query: string,
|
||||
operation: 'read' | 'write',
|
||||
): Promise<void> {
|
||||
// Extract table name from query
|
||||
const tableMatch = query.match(/(?:FROM|INTO|UPDATE|TABLE)\s+(\w+)/i)
|
||||
if (!tableMatch) {
|
||||
throw new Error('Could not extract table name from query')
|
||||
}
|
||||
|
||||
const tableName = tableMatch[1]
|
||||
|
||||
// Check if it's the extension's own table
|
||||
const extensionPrefix = `${extension.id}_${extension.name?.replace(/-/g, '_')}_`
|
||||
const isOwnTable = tableName.startsWith(extensionPrefix)
|
||||
|
||||
if (isOwnTable) {
|
||||
// Own tables: always allowed
|
||||
return
|
||||
}
|
||||
|
||||
// External table: Check permissions
|
||||
const hasPermission = await checkDatabasePermission(
|
||||
extension.id,
|
||||
tableName,
|
||||
operation,
|
||||
)
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new Error(`Permission denied: ${operation} access to ${tableName}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabasePermission(
|
||||
extensionId: string,
|
||||
tableName: string,
|
||||
operation: 'read' | 'write',
|
||||
): Promise<boolean> {
|
||||
// TODO: Query permissions from database
|
||||
// SELECT * FROM db_extension_permissions
|
||||
// WHERE extension_id = ? AND resource = ? AND operation = ?
|
||||
|
||||
console.warn('TODO: Implement permission check', {
|
||||
extensionId,
|
||||
tableName,
|
||||
operation,
|
||||
})
|
||||
|
||||
// For now: deny by default
|
||||
return false
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Methods
|
||||
// ==========================================
|
||||
|
||||
async function handlePermissionsMethod(
|
||||
async function handleFilesystemMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'permissions.database.request': {
|
||||
const params = request.params as {
|
||||
resource: string
|
||||
operation: 'read' | 'write'
|
||||
reason?: string
|
||||
}
|
||||
if (!request || !extension) return
|
||||
// TODO: Implementiere Filesystem Commands im Backend
|
||||
throw new Error('Filesystem methods not yet implemented')
|
||||
}
|
||||
|
||||
// TODO: Show user dialog to grant/deny permission
|
||||
console.log('[HaexHub] Permission request:', params)
|
||||
// ==========================================
|
||||
// HTTP Methods (TODO)
|
||||
// ==========================================
|
||||
|
||||
// For now: return ASK
|
||||
return {
|
||||
status: 'ask',
|
||||
permanent: false,
|
||||
}
|
||||
}
|
||||
|
||||
case 'permissions.database.check': {
|
||||
const params = request.params as {
|
||||
resource: string
|
||||
operation: 'read' | 'write'
|
||||
}
|
||||
|
||||
const hasPermission = await checkDatabasePermission(
|
||||
extension.value?.id || '',
|
||||
params.resource,
|
||||
params.operation,
|
||||
)
|
||||
|
||||
return {
|
||||
status: hasPermission ? 'granted' : 'denied',
|
||||
permanent: true,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown permission method: ${request.method}`)
|
||||
async function handleHttpMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
if (!extension || !request) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
// TODO: Implementiere HTTP Commands im Backend
|
||||
throw new Error('HTTP methods not yet implemented')
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Permission Methods (TODO)
|
||||
// ==========================================
|
||||
|
||||
async function handlePermissionsMethodAsync(
|
||||
request: ExtensionRequest,
|
||||
extension: IHaexHubExtension,
|
||||
) {
|
||||
if (!extension || !request) {
|
||||
throw new Error('Extension not found')
|
||||
}
|
||||
|
||||
// TODO: Implementiere Permission Request UI
|
||||
throw new Error('Permission methods not yet implemented')
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Context Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleContextMethod(request: ExtensionRequest) {
|
||||
const { theme } = useThemeStore()
|
||||
async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||
const { currentTheme } = storeToRefs(useUiStore())
|
||||
const { locale } = useI18n()
|
||||
|
||||
switch (request.method) {
|
||||
case 'context.get':
|
||||
return {
|
||||
theme: theme.value || 'system',
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform: detectPlatform(),
|
||||
}
|
||||
@ -330,29 +265,3 @@ function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
|
||||
if (width < 1024) return 'tablet'
|
||||
return 'desktop'
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Search Methods
|
||||
// ==========================================
|
||||
|
||||
async function handleSearchMethod(
|
||||
request: ExtensionRequest,
|
||||
extension: ComputedRef<IHaexHubExtensionLink | undefined>,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'search.respond': {
|
||||
const params = request.params as {
|
||||
requestId: string
|
||||
results: unknown[]
|
||||
}
|
||||
|
||||
// TODO: Store search results for display
|
||||
console.log('[HaexHub] Search results from extension:', params)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown search method: ${request.method}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<UPage
|
||||
<div
|
||||
:ui="{
|
||||
root: ['h-full w-full bg-elevated'],
|
||||
root: ['h-full w-full bg-elevated lg:flex'],
|
||||
center: ['h-full w-full'],
|
||||
}"
|
||||
>
|
||||
@ -34,7 +34,7 @@
|
||||
</template>
|
||||
</UiDialogConfirm>
|
||||
</div>
|
||||
</UPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -1,24 +1,65 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-scroll">
|
||||
<div
|
||||
v-if="!iFrameSrc"
|
||||
class="flex items-center justify-center h-full"
|
||||
>
|
||||
<p>{{ t('loading') }}</p>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Tab Bar -->
|
||||
<div class="flex gap-2 p-2 bg-default overflow-x-auto border-b">
|
||||
<div
|
||||
v-for="tab in tabsStore.sortedTabs"
|
||||
:key="tab.extension.id"
|
||||
:class="[
|
||||
'btn btn-sm gap-2',
|
||||
tabsStore.activeTabId === tab.extension.id
|
||||
? 'btn-primary'
|
||||
: 'btn-ghost',
|
||||
]"
|
||||
@click="tabsStore.setActiveTab(tab.extension.id)"
|
||||
>
|
||||
{{ tab.extension.name }}
|
||||
<button
|
||||
class="ml-1 hover:text-error"
|
||||
@click.stop="tabsStore.closeTab(tab.extension.id)"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:close"
|
||||
size="16"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IFrame Container -->
|
||||
<div class="flex-1 relative overflow-hidden">
|
||||
<div
|
||||
v-for="tab in tabsStore.sortedTabs"
|
||||
:key="tab.extension.id"
|
||||
:style="{ display: tab.isVisible ? 'block' : 'none' }"
|
||||
class="w-full h-full"
|
||||
>
|
||||
<iframe
|
||||
:ref="
|
||||
(el) => registerIFrame(tab.extension.id, el as HTMLIFrameElement)
|
||||
"
|
||||
class="w-full h-full"
|
||||
:src="getExtensionUrl(tab.extension)"
|
||||
sandbox="allow-scripts"
|
||||
allow="autoplay; speaker-selection; encrypted-media;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="tabsStore.tabCount === 0"
|
||||
class="flex items-center justify-center h-full"
|
||||
>
|
||||
<p>{{ t('loading') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
v-else
|
||||
ref="iFrameRef"
|
||||
class="w-full h-full"
|
||||
:src="iFrameSrc"
|
||||
sandbox="allow-scripts "
|
||||
allow="autoplay; speaker-selection; encrypted-media;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
|
||||
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
|
||||
definePageMeta({
|
||||
name: 'haexExtension',
|
||||
@ -26,42 +67,84 @@ definePageMeta({
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const iFrameRef = useTemplateRef('iFrameRef')
|
||||
const tabsStore = useExtensionTabsStore()
|
||||
|
||||
const { extensionEntry: iframeSrc, currentExtension } =
|
||||
storeToRefs(useExtensionsStore())
|
||||
// Extension aus Route öffnen
|
||||
//const extensionId = computed(() => route.params.extensionId as string)
|
||||
|
||||
const iFrameSrc = computed(() =>
|
||||
iframeSrc.value ? `${iframeSrc.value}/index.html` : '',
|
||||
const { currentExtensionId } = storeToRefs(useExtensionsStore())
|
||||
watchEffect(() => {
|
||||
if (currentExtensionId.value) {
|
||||
tabsStore.openTab(currentExtensionId.value)
|
||||
}
|
||||
})
|
||||
|
||||
const messageHandlers = new Map<string, boolean>()
|
||||
|
||||
watch(
|
||||
() => tabsStore.openTabs,
|
||||
(tabs) => {
|
||||
tabs.forEach((tab, id) => {
|
||||
if (tab.iframe && !messageHandlers.has(id)) {
|
||||
const iframeRef = ref(tab.iframe)
|
||||
const extensionRef = computed(() => tab.extension)
|
||||
useExtensionMessageHandler(iframeRef, extensionRef)
|
||||
messageHandlers.set(id, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
useExtensionMessageHandler(iFrameRef, currentExtension)
|
||||
// IFrame Registrierung und Message Handler Setup
|
||||
/* const iframeRefs = new Map<string, HTMLIFrameElement>()
|
||||
const setupMessageHandlers = new Set<string>() */
|
||||
|
||||
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
|
||||
if (!el) return
|
||||
tabsStore.registerIFrame(extensionId, el)
|
||||
}
|
||||
// Extension URL generieren
|
||||
const getExtensionUrl = (extension: IHaexHubExtension) => {
|
||||
const info = { id: extension.id, version: extension.version }
|
||||
const jsonString = JSON.stringify(info)
|
||||
const bytes = new TextEncoder().encode(jsonString)
|
||||
const encoded = Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
const url = `haex-extension://${encoded}/index.html`
|
||||
console.log('Extension URL:', url, 'for', extension.name)
|
||||
return url
|
||||
}
|
||||
|
||||
// Context Changes an alle Tabs broadcasten
|
||||
const { currentTheme } = storeToRefs(useUiStore())
|
||||
const { locale } = useI18n()
|
||||
|
||||
watch([currentTheme, locale], () => {
|
||||
if (iFrameRef.value?.contentWindow) {
|
||||
iFrameRef.value.contentWindow.postMessage(
|
||||
{
|
||||
type: 'context.changed',
|
||||
data: {
|
||||
context: {
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform:
|
||||
window.innerWidth < 768
|
||||
? 'mobile'
|
||||
: window.innerWidth < 1024
|
||||
? 'tablet'
|
||||
: 'desktop',
|
||||
},
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
tabsStore.broadcastToAllTabs({
|
||||
type: 'context.changed',
|
||||
data: {
|
||||
context: {
|
||||
theme: currentTheme.value || 'system',
|
||||
locale: locale.value,
|
||||
platform:
|
||||
window.innerWidth < 768
|
||||
? 'mobile'
|
||||
: window.innerWidth < 1024
|
||||
? 'tablet'
|
||||
: 'desktop',
|
||||
},
|
||||
'*',
|
||||
)
|
||||
}
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup beim Verlassen
|
||||
onBeforeUnmount(() => {
|
||||
// Optional: Alle Tabs schließen oder offen lassen
|
||||
// tabsStore.closeAllTabs()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -31,18 +31,17 @@
|
||||
class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3"
|
||||
/>
|
||||
<div class="fixed top-30 right-10">
|
||||
<UiTooltip :tooltip="t('extension.add')">
|
||||
<UiButton
|
||||
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
|
||||
@click="prepareInstallExtensionAsync"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:plus"
|
||||
size="1.5em"
|
||||
class="rotate-45"
|
||||
/>
|
||||
</UiButton>
|
||||
</UiTooltip>
|
||||
<UiButton
|
||||
class="btn-square btn-primary btn-xl btn-gradient rotate-45"
|
||||
:tooltip="t('extension.add')"
|
||||
@click="prepareInstallExtensionAsync"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:plus"
|
||||
size="1.5em"
|
||||
class="rotate-45"
|
||||
/>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -93,7 +92,7 @@ const extension = reactive<{
|
||||
path: '',
|
||||
})
|
||||
|
||||
const loadExtensionManifestAsync = async () => {
|
||||
/* const loadExtensionManifestAsync = async () => {
|
||||
try {
|
||||
extension.path = await open({ directory: true, recursive: true })
|
||||
if (!extension.path) return
|
||||
@ -111,7 +110,7 @@ const loadExtensionManifestAsync = async () => {
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
const { add } = useToast()
|
||||
const { addNotificationAsync } = useNotificationStore()
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<UPage>
|
||||
<div class="w-full">
|
||||
<div class="h-screen bg-amber-300 flex-1 flex-wrap">
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
</div>
|
||||
<div class="h-screen bg-teal-300 flex-1">
|
||||
abbbbbbbbbbbbbbbbbbbbb availableThemes:{{ uiStore.availableThemes }}
|
||||
</div>
|
||||
</UPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@ -1,22 +1,44 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { appDataDir, join } from '@tauri-apps/api/path'
|
||||
import { exists, readDir, readTextFile, remove } from '@tauri-apps/plugin-fs'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
|
||||
import type {
|
||||
IHaexHubExtension,
|
||||
IHaexHubExtensionLink,
|
||||
IHaexHubExtensionManifest,
|
||||
} from '~/types/haexhub'
|
||||
import { haexExtensions } from '~~/src-tauri/database/schemas/vault'
|
||||
|
||||
const manifestFileName = 'manifest.json'
|
||||
const logoFileName = 'icon.svg'
|
||||
interface ExtensionInfoResponse {
|
||||
key_hash: string
|
||||
name: string
|
||||
full_id: string
|
||||
version: string
|
||||
display_name: string | null
|
||||
namespace: string | null
|
||||
allowed_origin: string
|
||||
}
|
||||
|
||||
/* const manifestFileName = 'manifest.json'
|
||||
const logoFileName = 'icon.svg' */
|
||||
|
||||
export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
const availableExtensions = ref<IHaexHubExtensionLink[]>([])
|
||||
const { addNotificationAsync } = useNotificationStore()
|
||||
const availableExtensions = ref<IHaexHubExtension[]>([])
|
||||
const currentRoute = useRouter().currentRoute
|
||||
|
||||
const extensionLinks = computed<ISidebarItem[]>(() =>
|
||||
const currentExtensionId = computed(() =>
|
||||
getSingleRouteParam(currentRoute.value.params.extensionId),
|
||||
)
|
||||
|
||||
const currentExtension = computed(() => {
|
||||
if (!currentExtensionId.value) return null
|
||||
|
||||
return (
|
||||
availableExtensions.value.find(
|
||||
(ext) => ext.id === currentExtensionId.value,
|
||||
) ?? null
|
||||
)
|
||||
})
|
||||
|
||||
/* const { addNotificationAsync } = useNotificationStore() */
|
||||
|
||||
/* const extensionLinks = computed<ISidebarItem[]>(() =>
|
||||
availableExtensions.value
|
||||
.filter((extension) => extension.enabled && extension.installed)
|
||||
.map((extension) => ({
|
||||
@ -26,9 +48,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
tooltip: extension.name ?? '',
|
||||
to: { name: 'haexExtension', params: { extensionId: extension.id } },
|
||||
})),
|
||||
)
|
||||
|
||||
const currentRoute = useRouter().currentRoute
|
||||
) */
|
||||
|
||||
const isActive = (id: string) =>
|
||||
computed(
|
||||
@ -37,32 +57,26 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
currentRoute.value.params.extensionId === id,
|
||||
)
|
||||
|
||||
const currentExtension = computed(() => {
|
||||
console.log('computed currentExtension', currentRoute.value.params)
|
||||
if (currentRoute.value.meta.name !== 'haexExtension') return
|
||||
const extensionEntry = computed(() => {
|
||||
if (!currentExtension.value?.version || !currentExtension.value?.id)
|
||||
return null
|
||||
|
||||
const extensionId = getSingleRouteParam(
|
||||
currentRoute.value.params.extensionId,
|
||||
const encodedInfo = encodeExtensionInfo(
|
||||
currentExtension.value.id,
|
||||
currentExtension.value.version,
|
||||
)
|
||||
console.log('extensionId from param', extensionId)
|
||||
if (!extensionId) return
|
||||
|
||||
const extension = availableExtensions.value.find(
|
||||
(extension) => extension.id === extensionId,
|
||||
)
|
||||
console.log('currentExtension', extension)
|
||||
return extension
|
||||
return `extension://${encodedInfo}`
|
||||
})
|
||||
|
||||
const getExtensionPathAsync = async (
|
||||
/* const getExtensionPathAsync = async (
|
||||
extensionId?: string,
|
||||
version?: string,
|
||||
) => {
|
||||
if (!extensionId || !version) return ''
|
||||
return await join(await appDataDir(), 'extensions', extensionId, version)
|
||||
}
|
||||
} */
|
||||
|
||||
const checkSourceExtensionDirectoryAsync = async (
|
||||
/* const checkSourceExtensionDirectoryAsync = async (
|
||||
extensionDirectory: string,
|
||||
) => {
|
||||
try {
|
||||
@ -82,22 +96,154 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
//throw error //new Error(`Keine Leseberechtigung für Ordner ${extensionDirectory}`);
|
||||
}
|
||||
} */
|
||||
|
||||
const loadExtensionsAsync = async () => {
|
||||
try {
|
||||
const extensions =
|
||||
await invoke<ExtensionInfoResponse[]>('get_all_extensions')
|
||||
|
||||
availableExtensions.value = extensions.map((ext) => ({
|
||||
id: ext.key_hash,
|
||||
name: ext.display_name || ext.name,
|
||||
version: ext.version,
|
||||
author: ext.namespace,
|
||||
icon: null,
|
||||
enabled: true,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Extensions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const isExtensionInstalledAsync = async (
|
||||
extension: Partial<IHaexHubExtension>,
|
||||
) => {
|
||||
/* const loadExtensionsAsync = async () => {
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
|
||||
const extensions =
|
||||
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
|
||||
|
||||
//if (!extensions?.length) return false;
|
||||
|
||||
const installedExtensions = await filterAsync(
|
||||
extensions,
|
||||
isExtensionInstalledAsync,
|
||||
)
|
||||
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
|
||||
|
||||
availableExtensions.value =
|
||||
extensions.map((extension) => ({
|
||||
id: extension.id,
|
||||
name: extension.name ?? '',
|
||||
icon: extension.icon ?? '',
|
||||
author: extension.author ?? '',
|
||||
version: extension.version ?? '',
|
||||
enabled: extension.enabled ? true : false,
|
||||
installed: installedExtensions.includes(extension),
|
||||
})) ?? []
|
||||
|
||||
console.log('loadExtensionsAsync', availableExtensions.value)
|
||||
return true
|
||||
} */
|
||||
|
||||
const installAsync = async (sourcePath: string | null) => {
|
||||
if (!sourcePath) throw new Error('Kein Pfad angegeben')
|
||||
|
||||
try {
|
||||
const extensionPath = await getExtensionPathAsync(
|
||||
extension.id,
|
||||
`${extension.version}`,
|
||||
)
|
||||
console.log(
|
||||
`extension ${extension.id} is installed ${await exists(extensionPath)}`,
|
||||
)
|
||||
return await exists(extensionPath)
|
||||
const extensionId = await invoke<string>('install_extension', {
|
||||
sourcePath,
|
||||
})
|
||||
return extensionId
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error('Fehler bei Extension-Installation:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/* const installAsync = async (extensionDirectory: string | null) => {
|
||||
try {
|
||||
if (!extensionDirectory)
|
||||
throw new Error('Kein Ordner für Erweiterung angegeben')
|
||||
const manifestPath = await join(extensionDirectory, manifestFileName)
|
||||
const manifest = (await JSON.parse(
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
const destination = await getExtensionPathAsync(
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
)
|
||||
|
||||
await checkSourceExtensionDirectoryAsync(extensionDirectory)
|
||||
|
||||
await invoke('copy_directory', {
|
||||
source: extensionDirectory,
|
||||
destination,
|
||||
})
|
||||
|
||||
const logoFilePath = await join(destination, logoFileName)
|
||||
const logo = await readTextFile(logoFilePath)
|
||||
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
const res = await currentVault.value?.drizzle
|
||||
.insert(haexExtensions)
|
||||
.values({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
enabled: true,
|
||||
url: manifest.url,
|
||||
version: manifest.version,
|
||||
icon: logo,
|
||||
})
|
||||
|
||||
console.log('insert extensions', res)
|
||||
addNotificationAsync({
|
||||
type: 'success',
|
||||
text: `${manifest.name} wurde installiert`,
|
||||
})
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
throw error
|
||||
}
|
||||
} */
|
||||
|
||||
const removeExtensionAsync = async (extensionId: string, version: string) => {
|
||||
try {
|
||||
await invoke('remove_extension', {
|
||||
extensionId,
|
||||
extensionVersion: version,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen der Extension:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/* const removeExtensionAsync = async (id: string, version: string) => {
|
||||
try {
|
||||
console.log('remove extension', id, version)
|
||||
await removeExtensionFromVaultAsync(id, version)
|
||||
await removeExtensionFilesAsync(id, version)
|
||||
} catch (error) {
|
||||
throw new Error(JSON.stringify(error))
|
||||
}
|
||||
} */
|
||||
|
||||
const isExtensionInstalledAsync = async ({
|
||||
id,
|
||||
version,
|
||||
}: {
|
||||
id: string
|
||||
version: string
|
||||
}) => {
|
||||
try {
|
||||
return await invoke<boolean>('is_extension_installed', {
|
||||
extensionId: id,
|
||||
extensionVersion: version,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Prüfen der Extension:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -156,7 +302,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
const readManifestFileAsync = async (
|
||||
/* const readManifestFileAsync = async (
|
||||
extensionId: string,
|
||||
version: string,
|
||||
) => {
|
||||
@ -173,173 +319,17 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
/*
|
||||
TODO implement check, that manifest has valid data
|
||||
*/
|
||||
return manifest
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
console.error('ERROR readManifestFileAsync', error)
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
const installAsync = async (extensionDirectory: string | null) => {
|
||||
try {
|
||||
if (!extensionDirectory)
|
||||
throw new Error('Kein Ordner für Erweiterung angegeben')
|
||||
const manifestPath = await join(extensionDirectory, manifestFileName)
|
||||
const manifest = (await JSON.parse(
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
const destination = await getExtensionPathAsync(
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
)
|
||||
|
||||
await checkSourceExtensionDirectoryAsync(extensionDirectory)
|
||||
|
||||
await invoke('copy_directory', {
|
||||
source: extensionDirectory,
|
||||
destination,
|
||||
})
|
||||
|
||||
const logoFilePath = await join(destination, logoFileName)
|
||||
const logo = await readTextFile(logoFilePath)
|
||||
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
const res = await currentVault.value?.drizzle
|
||||
.insert(haexExtensions)
|
||||
.values({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
enabled: true,
|
||||
url: manifest.url,
|
||||
version: manifest.version,
|
||||
icon: logo,
|
||||
})
|
||||
|
||||
console.log('insert extensions', res)
|
||||
addNotificationAsync({
|
||||
type: 'success',
|
||||
text: `${manifest.name} wurde installiert`,
|
||||
})
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
throw error
|
||||
/*
|
||||
const resourcePath = await resourceDir();
|
||||
//const manifestPath = await join(extensionDirectory, 'manifest.json');
|
||||
const manifestPath = await join(
|
||||
resourcePath,
|
||||
'extension',
|
||||
'demo-addon',
|
||||
'manifest.json'
|
||||
);
|
||||
const regex = /((href|src)=["'])([^"']+)(["'])/g;
|
||||
let htmlContent = await readTextFile(
|
||||
await join(resourcePath, 'extension', 'demo-addon', 'index.html')
|
||||
);
|
||||
|
||||
const replacements = [];
|
||||
let match;
|
||||
while ((match = regex.exec(htmlContent)) !== null) {
|
||||
const [fullMatch, prefix, attr, resource, suffix] = match;
|
||||
if (!resource.startsWith('http')) {
|
||||
replacements.push({ match: fullMatch, resource, prefix, suffix });
|
||||
}
|
||||
}
|
||||
|
||||
for (const { match, resource, prefix, suffix } of replacements) {
|
||||
const fileContent = await readTextFile(
|
||||
await join(resourcePath, 'extension', 'demo-addon', resource)
|
||||
);
|
||||
const blob = new Blob([fileContent], { type: getMimeType(resource) });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
console.log('blob', resource, blobUrl);
|
||||
htmlContent = htmlContent.replace(
|
||||
match,
|
||||
`${prefix}${blobUrl}${suffix}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('htmlContent', htmlContent);
|
||||
|
||||
const blob = new Blob([htmlContent], { type: 'text/html' });
|
||||
const iframeSrc = URL.createObjectURL(blob);
|
||||
|
||||
const manifestContent = await readTextFile(manifestPath);
|
||||
console.log('iframeSrc', iframeSrc);
|
||||
const manifest: PluginManifest = JSON.parse(manifestContent);
|
||||
//const entryPath = await join(extensionDirectory, manifest.entry);
|
||||
const entryPath = await join(
|
||||
resourcePath,
|
||||
'extension',
|
||||
'demo-addon',
|
||||
manifest.entry
|
||||
);
|
||||
console.log('extensionDirectory', extensionDirectory, entryPath);
|
||||
const path = convertFileSrc(extensionDirectory, manifest.entry);
|
||||
console.log('final path', path);
|
||||
manifest.entry = iframeSrc;
|
||||
/* await join(
|
||||
path, //`file:/${extensionDirectory}`,
|
||||
manifest.entry
|
||||
); */
|
||||
// Modul-Datei laden
|
||||
//const modulePathFull = await join(basePath, manifest.main);
|
||||
/* const manifest: PluginManifest = await invoke('load_plugin', {
|
||||
manifestPath,
|
||||
}); */
|
||||
/* const iframe = document.createElement('iframe');
|
||||
iframe.src = manifest.entry;
|
||||
iframe.setAttribute('sandbox', 'allow-scripts');
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.border = 'none'; */
|
||||
/* const addonApi = {
|
||||
db_execute: async (sql: string, params: string[] = []) => {
|
||||
return invoke('db_execute', {
|
||||
addonId: manifest.name,
|
||||
sql,
|
||||
params,
|
||||
});
|
||||
},
|
||||
db_select: async (sql: string, params: string[] = []) => {
|
||||
return invoke('db_select', {
|
||||
addonId: manifest.name,
|
||||
sql,
|
||||
params,
|
||||
});
|
||||
},
|
||||
}; */
|
||||
/* iframe.onload = () => {
|
||||
iframe.contentWindow?.postMessage(
|
||||
{ type: 'init', payload: addonApi },
|
||||
'*'
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source === iframe.contentWindow) {
|
||||
const { type } = event.data;
|
||||
if (type === 'ready') {
|
||||
console.log(`Plugin ${manifest.name} ist bereit`);
|
||||
}
|
||||
}
|
||||
}); */
|
||||
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
|
||||
|
||||
console.log(`Plugin ${manifest.name} geladen.`); */
|
||||
}
|
||||
}
|
||||
|
||||
const extensionEntry = computedAsync(
|
||||
/* const extensionEntry = computedAsync(
|
||||
async () => {
|
||||
try {
|
||||
/* console.log("extensionEntry start", currentExtension.value);
|
||||
const regex = /((href|src)=["'])([^"']+)(["'])/g; */
|
||||
|
||||
|
||||
if (!currentExtension.value?.id || !currentExtension.value.version) {
|
||||
console.log('extension id or entry missing', currentExtension.value)
|
||||
@ -375,60 +365,19 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
},
|
||||
null,
|
||||
{ lazy: true },
|
||||
)
|
||||
|
||||
const loadExtensionsAsync = async () => {
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
|
||||
const extensions =
|
||||
(await currentVault.value?.drizzle.select().from(haexExtensions)) ?? []
|
||||
|
||||
//if (!extensions?.length) return false;
|
||||
|
||||
const installedExtensions = await filterAsync(
|
||||
extensions,
|
||||
isExtensionInstalledAsync,
|
||||
)
|
||||
console.log('loadExtensionsAsync installedExtensions', installedExtensions)
|
||||
|
||||
availableExtensions.value =
|
||||
extensions.map((extension) => ({
|
||||
id: extension.id,
|
||||
name: extension.name ?? '',
|
||||
icon: extension.icon ?? '',
|
||||
author: extension.author ?? '',
|
||||
version: extension.version ?? '',
|
||||
enabled: extension.enabled ? true : false,
|
||||
installed: installedExtensions.includes(extension),
|
||||
})) ?? []
|
||||
|
||||
console.log('loadExtensionsAsync', availableExtensions.value)
|
||||
return true
|
||||
}
|
||||
|
||||
const removeExtensionAsync = async (id: string, version: string) => {
|
||||
try {
|
||||
console.log('remove extension', id, version)
|
||||
await removeExtensionFromVaultAsync(id, version)
|
||||
await removeExtensionFilesAsync(id, version)
|
||||
} catch (error) {
|
||||
throw new Error(JSON.stringify(error))
|
||||
}
|
||||
}
|
||||
) */
|
||||
|
||||
return {
|
||||
availableExtensions,
|
||||
checkManifest,
|
||||
currentExtension,
|
||||
currentExtensionId,
|
||||
extensionEntry,
|
||||
extensionLinks,
|
||||
installAsync,
|
||||
isActive,
|
||||
isExtensionInstalledAsync,
|
||||
loadExtensionsAsync,
|
||||
readManifestFileAsync,
|
||||
removeExtensionAsync,
|
||||
getExtensionPathAsync,
|
||||
}
|
||||
})
|
||||
|
||||
@ -438,7 +387,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
return 'text/plain'
|
||||
} */
|
||||
|
||||
const removeExtensionFromVaultAsync = async (
|
||||
/* const removeExtensionFromVaultAsync = async (
|
||||
id: string | null,
|
||||
version: string | null,
|
||||
) => {
|
||||
@ -457,9 +406,9 @@ const removeExtensionFromVaultAsync = async (
|
||||
.delete(haexExtensions)
|
||||
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)))
|
||||
return removedExtensions
|
||||
}
|
||||
} */
|
||||
|
||||
const removeExtensionFilesAsync = async (
|
||||
/* const removeExtensionFilesAsync = async (
|
||||
id: string | null,
|
||||
version: string | null,
|
||||
) => {
|
||||
@ -483,4 +432,13 @@ const removeExtensionFilesAsync = async (
|
||||
console.error('ERROR removeExtensionFilesAsync', error)
|
||||
throw new Error(JSON.stringify(error))
|
||||
}
|
||||
} */
|
||||
|
||||
function encodeExtensionInfo(id: string, version: string): string {
|
||||
const info = { id, version }
|
||||
const jsonString = JSON.stringify(info)
|
||||
const bytes = new TextEncoder().encode(jsonString)
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
143
src/stores/extensions/tabs.ts
Normal file
143
src/stores/extensions/tabs.ts
Normal file
@ -0,0 +1,143 @@
|
||||
// stores/extensions/tabs.ts
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
|
||||
interface ExtensionTab {
|
||||
extension: IHaexHubExtension
|
||||
iframe: HTMLIFrameElement | null
|
||||
isVisible: boolean
|
||||
lastAccessed: number
|
||||
}
|
||||
|
||||
export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
||||
// State
|
||||
const openTabs = ref(new Map<string, ExtensionTab>())
|
||||
const activeTabId = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const activeTab = computed(() => {
|
||||
if (!activeTabId.value) return null
|
||||
return openTabs.value.get(activeTabId.value) || null
|
||||
})
|
||||
|
||||
const tabCount = computed(() => openTabs.value.size)
|
||||
|
||||
const sortedTabs = computed(() => {
|
||||
return Array.from(openTabs.value.values()).sort(
|
||||
(a, b) => b.lastAccessed - a.lastAccessed,
|
||||
)
|
||||
})
|
||||
|
||||
// Actions
|
||||
const openTab = (extensionId: string) => {
|
||||
// Hole Extension-Info aus dem anderen Store
|
||||
const extensionsStore = useExtensionsStore()
|
||||
const extension = extensionsStore.availableExtensions.find(
|
||||
(ext) => ext.id === extensionId,
|
||||
)
|
||||
|
||||
if (!extension) {
|
||||
console.error(`Extension ${extensionId} nicht gefunden`)
|
||||
return
|
||||
}
|
||||
|
||||
// Bereits geöffnet? Nur aktivieren
|
||||
if (openTabs.value.has(extensionId)) {
|
||||
setActiveTab(extensionId)
|
||||
return
|
||||
}
|
||||
|
||||
// Limit: Max 10 Tabs
|
||||
if (openTabs.value.size >= 10) {
|
||||
const oldestInactive = sortedTabs.value
|
||||
.filter((tab) => tab.extension.id !== activeTabId.value)
|
||||
.pop()
|
||||
|
||||
if (oldestInactive) {
|
||||
closeTab(oldestInactive.extension.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Neuen Tab erstellen
|
||||
openTabs.value.set(extensionId, {
|
||||
extension,
|
||||
iframe: null,
|
||||
isVisible: false,
|
||||
lastAccessed: Date.now(),
|
||||
})
|
||||
|
||||
setActiveTab(extensionId)
|
||||
}
|
||||
|
||||
const setActiveTab = (extensionId: string) => {
|
||||
// Verstecke aktuellen Tab
|
||||
if (activeTabId.value && openTabs.value.has(activeTabId.value)) {
|
||||
const currentTab = openTabs.value.get(activeTabId.value)!
|
||||
currentTab.isVisible = false
|
||||
}
|
||||
|
||||
// Zeige neuen Tab
|
||||
const newTab = openTabs.value.get(extensionId)
|
||||
if (newTab) {
|
||||
newTab.isVisible = true
|
||||
newTab.lastAccessed = Date.now()
|
||||
activeTabId.value = extensionId
|
||||
}
|
||||
}
|
||||
|
||||
const closeTab = (extensionId: string) => {
|
||||
const tab = openTabs.value.get(extensionId)
|
||||
if (!tab) return
|
||||
|
||||
// IFrame entfernen
|
||||
tab.iframe?.remove()
|
||||
openTabs.value.delete(extensionId)
|
||||
|
||||
// Nächsten Tab aktivieren
|
||||
if (activeTabId.value === extensionId) {
|
||||
const remaining = sortedTabs.value
|
||||
const nextTab = remaining[0]
|
||||
|
||||
if (nextTab) {
|
||||
setActiveTab(nextTab.extension.id)
|
||||
} else {
|
||||
activeTabId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registerIFrame = (extensionId: string, iframe: HTMLIFrameElement) => {
|
||||
const tab = openTabs.value.get(extensionId)
|
||||
if (tab) {
|
||||
tab.iframe = iframe
|
||||
}
|
||||
}
|
||||
|
||||
const broadcastToAllTabs = (message: unknown) => {
|
||||
openTabs.value.forEach(({ iframe }) => {
|
||||
iframe?.contentWindow?.postMessage(message, '*')
|
||||
})
|
||||
}
|
||||
|
||||
const closeAllTabs = () => {
|
||||
openTabs.value.forEach((tab) => tab.iframe?.remove())
|
||||
openTabs.value.clear()
|
||||
activeTabId.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
openTabs,
|
||||
activeTabId,
|
||||
// Getters
|
||||
activeTab,
|
||||
tabCount,
|
||||
sortedTabs,
|
||||
// Actions
|
||||
openTab,
|
||||
setActiveTab,
|
||||
closeTab,
|
||||
registerIFrame,
|
||||
broadcastToAllTabs,
|
||||
closeAllTabs,
|
||||
}
|
||||
})
|
||||
11
src/types/haexhub.d.ts
vendored
11
src/types/haexhub.d.ts
vendored
@ -25,11 +25,10 @@ export interface IHaexHubExtensionLink extends IHaexHubExtension {
|
||||
}
|
||||
|
||||
export interface IHaexHubExtension {
|
||||
author?: string | null
|
||||
enabled?: boolean | null
|
||||
icon?: string | null
|
||||
id: string
|
||||
manifest?: IHaexHubExtensionManifest
|
||||
name: string | null
|
||||
version?: string | null
|
||||
name: string
|
||||
version: string
|
||||
author: string | null
|
||||
icon: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user