mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-19 23:30:51 +01:00
use window system
This commit is contained in:
@ -1,459 +0,0 @@
|
||||
<template>
|
||||
<div class="h-screen w-screen flex flex-col">
|
||||
<!-- Tab Bar -->
|
||||
<div
|
||||
class="flex gap-2 bg-base-200 overflow-x-auto border-b border-base-300 flex-shrink-0"
|
||||
>
|
||||
<UButton
|
||||
v-for="tab in tabsStore.sortedTabs"
|
||||
:key="tab.extension.id"
|
||||
:class="[
|
||||
'gap-2',
|
||||
tabsStore.activeTabId === tab.extension.id ? 'primary' : 'neutral',
|
||||
]"
|
||||
@click="tabsStore.setActiveTab(tab.extension.id)"
|
||||
>
|
||||
{{ tab.extension.name }}
|
||||
|
||||
<template #trailing>
|
||||
<div
|
||||
class="ml-1 hover:text-error"
|
||||
@click.stop="tabsStore.closeTab(tab.extension.id)"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:close"
|
||||
size="16"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UButton>
|
||||
|
||||
<!-- Console Tab -->
|
||||
<UButton
|
||||
:class="['gap-2', showConsole ? 'primary' : 'neutral']"
|
||||
@click="showConsole = !showConsole"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:console"
|
||||
size="16"
|
||||
/>
|
||||
Console
|
||||
<UBadge
|
||||
v-if="visibleLogs.length > 0"
|
||||
size="xs"
|
||||
color="primary"
|
||||
>
|
||||
{{ visibleLogs.length }}
|
||||
</UBadge>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- IFrame Container -->
|
||||
<div class="flex-1 relative min-h-0">
|
||||
<!-- Extension IFrames -->
|
||||
<div
|
||||
v-for="tab in tabsStore.sortedTabs"
|
||||
:key="tab.extension.id"
|
||||
: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.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>
|
||||
|
||||
<!-- Console View -->
|
||||
<div
|
||||
v-if="showConsole"
|
||||
class="absolute inset-0 bg-base-100 flex flex-col"
|
||||
>
|
||||
<!-- Console Header -->
|
||||
<div
|
||||
class="p-2 border-b border-base-300 flex justify-between items-center"
|
||||
>
|
||||
<h3 class="font-semibold">Console Output</h3>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
@click="$clearConsoleLogs()"
|
||||
>
|
||||
Clear
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- Console Logs -->
|
||||
<div class="flex-1 overflow-y-auto p-2 font-mono text-sm">
|
||||
<!-- Info banner if logs are limited -->
|
||||
<div
|
||||
v-if="consoleLogs.length > maxVisibleLogs"
|
||||
class="mb-2 p-2 bg-warning/10 border border-warning/30 rounded text-xs"
|
||||
>
|
||||
Showing last {{ maxVisibleLogs }} of {{ consoleLogs.length }} logs
|
||||
</div>
|
||||
|
||||
<!-- Simple log list instead of accordion for better performance -->
|
||||
<div
|
||||
v-if="visibleLogs.length > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div
|
||||
v-for="(log, index) in visibleLogs"
|
||||
:key="index"
|
||||
class="border-b border-base-200 pb-2"
|
||||
>
|
||||
<!-- Log header with timestamp and level -->
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-xs opacity-60">
|
||||
[{{ log.timestamp }}] [{{ log.level.toUpperCase() }}]
|
||||
</span>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-clipboard-document"
|
||||
@click="copyToClipboard(log.message)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Log message -->
|
||||
<pre
|
||||
:class="[
|
||||
'text-xs whitespace-pre-wrap break-all',
|
||||
log.level === 'error' ? 'text-error' : '',
|
||||
log.level === 'warn' ? 'text-warning' : '',
|
||||
log.level === 'info' ? 'text-info' : '',
|
||||
log.level === 'debug' ? 'text-base-content/70' : '',
|
||||
]"
|
||||
>{{ log.message }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="visibleLogs.length === 0"
|
||||
class="text-center text-base-content/50 py-8"
|
||||
>
|
||||
No console messages yet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="tabsStore.tabCount === 0"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<p>{{ t('loading') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EXTENSION_PROTOCOL_PREFIX,
|
||||
EXTENSION_PROTOCOL_NAME,
|
||||
} from '~/config/constants'
|
||||
|
||||
definePageMeta({
|
||||
name: 'extension',
|
||||
})
|
||||
|
||||
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)
|
||||
const maxVisibleLogs = ref(100) // Limit for performance on mobile
|
||||
const consoleLogs = $consoleLogs as Ref<
|
||||
Array<{
|
||||
timestamp: string
|
||||
level: 'log' | 'info' | 'warn' | 'error' | 'debug'
|
||||
message: string
|
||||
}>
|
||||
>
|
||||
|
||||
// Only show last N logs for performance
|
||||
const visibleLogs = computed(() => {
|
||||
return consoleLogs.value.slice(-maxVisibleLogs.value)
|
||||
})
|
||||
|
||||
// Extension aus Route öffnen
|
||||
//const extensionId = computed(() => route.params.extensionId as string)
|
||||
|
||||
const { currentExtensionId } = storeToRefs(useExtensionsStore())
|
||||
watchEffect(() => {
|
||||
if (currentExtensionId.value) {
|
||||
tabsStore.openTab(currentExtensionId.value)
|
||||
}
|
||||
})
|
||||
|
||||
// Setup global message handler EINMAL im Setup-Kontext
|
||||
// Dies registriert den globalen Event Listener
|
||||
const dummyIframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
const dummyExtensionRef = computed(() => null)
|
||||
useExtensionMessageHandler(dummyIframeRef, dummyExtensionRef)
|
||||
|
||||
// Track which iframes have been registered to prevent duplicate registrations
|
||||
const registeredIFrames = new WeakSet<HTMLIFrameElement>()
|
||||
|
||||
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
|
||||
if (!el) return
|
||||
|
||||
// Prevent duplicate registration (Vue calls ref functions on every render)
|
||||
if (registeredIFrames.has(el)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[Vue Debug] ========== registerIFrame called ==========')
|
||||
console.log('[Vue Debug] Extension ID:', extensionId)
|
||||
console.log('[Vue Debug] Element:', 'HTMLIFrameElement')
|
||||
|
||||
// Mark as registered
|
||||
registeredIFrames.add(el)
|
||||
|
||||
// Registriere IFrame im Store
|
||||
tabsStore.registerIFrame(extensionId, el)
|
||||
|
||||
// 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,
|
||||
)
|
||||
registerExtensionIFrame(el, tab.extension)
|
||||
console.log('[Vue Debug] Registration complete!')
|
||||
} else {
|
||||
console.error('[Vue Debug] ❌ No tab found for extension ID:', extensionId)
|
||||
}
|
||||
console.log('[Vue Debug] ========================================')
|
||||
}
|
||||
|
||||
// Listen for console messages from extensions (via postMessage)
|
||||
const handleExtensionConsole = (event: MessageEvent) => {
|
||||
if (event.data?.type === 'console.forward') {
|
||||
const { timestamp, level, message } = event.data.data
|
||||
consoleLogs.value.push({
|
||||
timestamp,
|
||||
level,
|
||||
message: `[Extension] ${message}`,
|
||||
})
|
||||
|
||||
// Limit to last 1000 logs
|
||||
if (consoleLogs.value.length > 1000) {
|
||||
consoleLogs.value = consoleLogs.value.slice(-1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', handleExtensionConsole)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', handleExtensionConsole)
|
||||
|
||||
// Unregister all iframes when the page unmounts
|
||||
tabsStore.openTabs.forEach((tab) => {
|
||||
if (tab.iframe) {
|
||||
unregisterExtensionIFrame(tab.iframe)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup wenn Tabs geschlossen werden
|
||||
watch(
|
||||
() => tabsStore.openTabs,
|
||||
(newTabs, oldTabs) => {
|
||||
if (oldTabs) {
|
||||
// Finde gelöschte Tabs
|
||||
oldTabs.forEach((tab, id) => {
|
||||
if (!newTabs.has(id) && tab.iframe) {
|
||||
unregisterExtensionIFrame(tab.iframe)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Context Changes an alle Tabs broadcasten
|
||||
const { currentTheme } = storeToRefs(useUiStore())
|
||||
const { locale } = useI18n()
|
||||
|
||||
watch([currentTheme, locale], () => {
|
||||
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(),
|
||||
})
|
||||
})
|
||||
|
||||
// Copy to clipboard function
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
// Optional: Show success toast
|
||||
console.log('Copied to clipboard')
|
||||
} catch (err) {
|
||||
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>
|
||||
@ -1,599 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header with Actions -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 p-6 border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">
|
||||
{{ t('title') }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ t('subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3"
|
||||
>
|
||||
<!-- Marketplace Selector -->
|
||||
<USelectMenu
|
||||
v-model="selectedMarketplace"
|
||||
:items="marketplaces"
|
||||
value-key="id"
|
||||
class="w-full sm:w-48"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-heroicons-building-storefront" />
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
<!-- Install from File Button -->
|
||||
<UiButton
|
||||
:label="t('extension.installFromFile')"
|
||||
icon="i-heroicons-arrow-up-tray"
|
||||
color="neutral"
|
||||
@click="onSelectExtensionAsync"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-stretch sm:items-center gap-4 p-6 border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
<UInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="t('search.placeholder')"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
class="flex-1"
|
||||
/>
|
||||
<USelectMenu
|
||||
v-model="selectedCategory"
|
||||
:items="categories"
|
||||
:placeholder="t('filter.category')"
|
||||
value-key="id"
|
||||
class="w-full sm:w-48"
|
||||
>
|
||||
<template #leading>
|
||||
<UIcon name="i-heroicons-tag" />
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
|
||||
<!-- Extensions Grid -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div
|
||||
v-if="filteredExtensions.length"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
<!-- Marketplace Extension Card -->
|
||||
<HaexExtensionMarketplaceCard
|
||||
v-for="ext in filteredExtensions"
|
||||
:key="ext.id"
|
||||
:extension="ext"
|
||||
@install="onInstallFromMarketplace(ext)"
|
||||
@details="onShowExtensionDetails(ext)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center h-full text-center"
|
||||
>
|
||||
<UIcon
|
||||
name="i-heroicons-magnifying-glass"
|
||||
class="w-16 h-16 text-gray-400 mb-4"
|
||||
/>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('empty.title') }}
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mt-2">
|
||||
{{ t('empty.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HaexExtensionDialogReinstall
|
||||
v-model:open="openOverwriteDialog"
|
||||
v-model:preview="preview"
|
||||
@confirm="reinstallExtensionAsync"
|
||||
/>
|
||||
|
||||
<HaexExtensionDialogInstall
|
||||
v-model:open="showConfirmation"
|
||||
:preview="preview"
|
||||
@confirm="(addToDesktop) => addExtensionAsync(addToDesktop)"
|
||||
/>
|
||||
|
||||
<HaexExtensionDialogRemove
|
||||
v-model:open="showRemoveDialog"
|
||||
:extension="extensionToBeRemoved"
|
||||
@confirm="removeExtensionAsync"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
IHaexHubExtension,
|
||||
IHaexHubExtensionManifest,
|
||||
IMarketplaceExtension,
|
||||
} from '~/types/haexhub'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
|
||||
|
||||
definePageMeta({
|
||||
name: 'extensionOverview',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const extensionStore = useExtensionsStore()
|
||||
const desktopStore = useDesktopStore()
|
||||
|
||||
const showConfirmation = ref(false)
|
||||
const openOverwriteDialog = ref(false)
|
||||
|
||||
const extension = reactive<{
|
||||
manifest: IHaexHubExtensionManifest | null | undefined
|
||||
path: string | null
|
||||
}>({
|
||||
manifest: null,
|
||||
path: '',
|
||||
})
|
||||
|
||||
/* const loadExtensionManifestAsync = async () => {
|
||||
try {
|
||||
extension.path = await open({ directory: true, recursive: true })
|
||||
if (!extension.path) return
|
||||
|
||||
const manifestFile = JSON.parse(
|
||||
await readTextFile(await join(extension.path, 'manifest.json')),
|
||||
)
|
||||
|
||||
if (!extensionStore.checkManifest(manifestFile))
|
||||
throw new Error(`Manifest fehlerhaft ${JSON.stringify(manifestFile)}`)
|
||||
|
||||
return manifestFile
|
||||
} catch (error) {
|
||||
console.error('Fehler loadExtensionManifestAsync:', error)
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||
}
|
||||
} */
|
||||
|
||||
const { add } = useToast()
|
||||
const { addNotificationAsync } = useNotificationStore()
|
||||
|
||||
const preview = ref<ExtensionPreview>()
|
||||
|
||||
// Marketplace State
|
||||
const selectedMarketplace = ref('official')
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('all')
|
||||
|
||||
// Marketplaces (später von API laden)
|
||||
const marketplaces = [
|
||||
{
|
||||
id: 'official',
|
||||
label: t('marketplace.official'),
|
||||
icon: 'i-heroicons-building-storefront',
|
||||
},
|
||||
{
|
||||
id: 'community',
|
||||
label: t('marketplace.community'),
|
||||
icon: 'i-heroicons-users',
|
||||
},
|
||||
]
|
||||
|
||||
// Categories
|
||||
const categories = computed(() => [
|
||||
{ id: 'all', label: t('category.all') },
|
||||
{ id: 'productivity', label: t('category.productivity') },
|
||||
{ id: 'security', label: t('category.security') },
|
||||
{ id: 'utilities', label: t('category.utilities') },
|
||||
{ id: 'integration', label: t('category.integration') },
|
||||
])
|
||||
|
||||
// Dummy Marketplace Extensions (später von API laden)
|
||||
const marketplaceExtensions = ref<IMarketplaceExtension[]>([
|
||||
{
|
||||
id: 'haex-passy',
|
||||
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',
|
||||
homepage: null,
|
||||
downloads: 15420,
|
||||
rating: 4.8,
|
||||
verified: true,
|
||||
tags: ['security', 'password', 'productivity'],
|
||||
category: 'security',
|
||||
downloadUrl: '/extensions/haex-pass-1.0.0.haextension',
|
||||
isInstalled: false,
|
||||
},
|
||||
{
|
||||
id: 'haex-notes',
|
||||
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',
|
||||
homepage: null,
|
||||
downloads: 8930,
|
||||
rating: 4.5,
|
||||
verified: true,
|
||||
tags: ['productivity', 'notes', 'markdown'],
|
||||
category: 'productivity',
|
||||
downloadUrl: '/extensions/haex-notes-2.1.0.haextension',
|
||||
isInstalled: false,
|
||||
},
|
||||
{
|
||||
id: 'haex-backup',
|
||||
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',
|
||||
homepage: null,
|
||||
downloads: 5240,
|
||||
rating: 4.6,
|
||||
verified: false,
|
||||
tags: ['backup', 'cloud', 'utilities'],
|
||||
category: 'utilities',
|
||||
downloadUrl: '/extensions/haex-backup-1.5.2.haextension',
|
||||
isInstalled: false,
|
||||
},
|
||||
{
|
||||
id: 'haex-calendar',
|
||||
name: 'HaexCalendar',
|
||||
version: '3.0.1',
|
||||
author: 'HaexHub Team',
|
||||
public_key: 'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5',
|
||||
description:
|
||||
'Integrierter Kalender mit Event-Management und Synchronisation.',
|
||||
icon: 'i-heroicons-calendar',
|
||||
homepage: null,
|
||||
downloads: 12100,
|
||||
rating: 4.7,
|
||||
verified: true,
|
||||
tags: ['productivity', 'calendar', 'events'],
|
||||
category: 'productivity',
|
||||
downloadUrl: '/extensions/haex-calendar-3.0.1.haextension',
|
||||
isInstalled: false,
|
||||
},
|
||||
{
|
||||
id: 'haex-2fa',
|
||||
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',
|
||||
homepage: null,
|
||||
downloads: 7800,
|
||||
rating: 4.9,
|
||||
verified: true,
|
||||
tags: ['security', '2fa', 'authentication'],
|
||||
category: 'security',
|
||||
downloadUrl: '/extensions/haex-2fa-1.2.0.haextension',
|
||||
isInstalled: false,
|
||||
},
|
||||
{
|
||||
id: 'haex-github',
|
||||
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',
|
||||
homepage: null,
|
||||
downloads: 4120,
|
||||
rating: 4.3,
|
||||
verified: false,
|
||||
tags: ['integration', 'github', 'development'],
|
||||
category: 'integration',
|
||||
downloadUrl: '/extensions/haex-github-1.0.5.haextension',
|
||||
isInstalled: false,
|
||||
},
|
||||
])
|
||||
|
||||
// Mark marketplace extensions as installed if they exist in availableExtensions
|
||||
const allExtensions = computed((): IMarketplaceExtension[] => {
|
||||
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
|
||||
const filteredExtensions = computed(() => {
|
||||
return allExtensions.value.filter((ext) => {
|
||||
const matchesSearch =
|
||||
!searchQuery.value ||
|
||||
ext.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
ext.description?.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
|
||||
const matchesCategory =
|
||||
selectedCategory.value === 'all' ||
|
||||
ext.category === selectedCategory.value
|
||||
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
})
|
||||
|
||||
// Install from marketplace
|
||||
const onInstallFromMarketplace = async (ext: unknown) => {
|
||||
console.log('Install from marketplace:', ext)
|
||||
// TODO: Download extension from marketplace and install
|
||||
add({ color: 'info', description: t('extension.marketplace.comingSoon') })
|
||||
}
|
||||
|
||||
// Show extension details
|
||||
const onShowExtensionDetails = (ext: unknown) => {
|
||||
console.log('Show details:', ext)
|
||||
// TODO: Show extension details modal
|
||||
}
|
||||
|
||||
const onSelectExtensionAsync = async () => {
|
||||
try {
|
||||
extension.path = await open({ directory: false, recursive: true })
|
||||
if (!extension.path) return
|
||||
|
||||
preview.value = await extensionStore.previewManifestAsync(extension.path)
|
||||
|
||||
if (!preview.value?.manifest) return
|
||||
|
||||
// Check if already installed using public_key + name
|
||||
const isAlreadyInstalled = extensionStore.availableExtensions.some(
|
||||
(ext) =>
|
||||
ext.publicKey === preview.value!.manifest.public_key &&
|
||||
ext.name === preview.value!.manifest.name
|
||||
)
|
||||
|
||||
if (isAlreadyInstalled) {
|
||||
openOverwriteDialog.value = true
|
||||
} else {
|
||||
showConfirmation.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const addExtensionAsync = async (addToDesktop: boolean = false) => {
|
||||
try {
|
||||
console.log(
|
||||
'preview.value?.editable_permissions',
|
||||
preview.value?.editable_permissions,
|
||||
)
|
||||
const extensionId = await extensionStore.installAsync(
|
||||
extension.path,
|
||||
preview.value?.editable_permissions,
|
||||
)
|
||||
await extensionStore.loadExtensionsAsync()
|
||||
|
||||
// Add to desktop if requested
|
||||
if (addToDesktop && extensionId) {
|
||||
await desktopStore.addDesktopItemAsync('extension', extensionId, 50, 50)
|
||||
}
|
||||
|
||||
add({
|
||||
color: 'success',
|
||||
title: t('extension.success.title', {
|
||||
extension: extension.manifest?.name,
|
||||
}),
|
||||
description: t('extension.success.text'),
|
||||
})
|
||||
await addNotificationAsync({
|
||||
text: t('extension.success.text'),
|
||||
type: 'success',
|
||||
title: t('extension.success.title', {
|
||||
extension: extension.manifest?.name,
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Fehler addExtensionAsync:', error)
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const reinstallExtensionAsync = async () => {
|
||||
try {
|
||||
if (!preview.value?.manifest) return
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
if (installedExt) {
|
||||
// Remove old extension first
|
||||
await extensionStore.removeExtensionAsync(
|
||||
installedExt.publicKey,
|
||||
installedExt.name,
|
||||
installedExt.version
|
||||
)
|
||||
}
|
||||
|
||||
// Then install new version
|
||||
await addExtensionAsync()
|
||||
} catch (error) {
|
||||
console.error('Fehler reinstallExtensionAsync:', error)
|
||||
add({ color: 'error', description: JSON.stringify(error) })
|
||||
await addNotificationAsync({ text: JSON.stringify(error), type: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
const extensionToBeRemoved = ref<IHaexHubExtension>()
|
||||
const showRemoveDialog = ref(false)
|
||||
|
||||
// Load extensions on mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await extensionStore.loadExtensionsAsync()
|
||||
console.log('Loaded extensions:', extensionStore.availableExtensions)
|
||||
} catch (error) {
|
||||
console.error('Failed to load extensions:', error)
|
||||
add({ color: 'error', description: 'Failed to load installed extensions' })
|
||||
}
|
||||
})
|
||||
|
||||
/* const onShowRemoveDialog = (extension: IHaexHubExtension) => {
|
||||
extensionToBeRemoved.value = extension
|
||||
showRemoveDialog.value = true
|
||||
} */
|
||||
|
||||
const removeExtensionAsync = async () => {
|
||||
if (!extensionToBeRemoved.value?.publicKey || !extensionToBeRemoved.value?.name || !extensionToBeRemoved.value?.version) {
|
||||
add({
|
||||
color: 'error',
|
||||
description: 'Erweiterung kann nicht gelöscht werden',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await extensionStore.removeExtensionAsync(
|
||||
extensionToBeRemoved.value.publicKey,
|
||||
extensionToBeRemoved.value.name,
|
||||
extensionToBeRemoved.value.version,
|
||||
)
|
||||
await extensionStore.loadExtensionsAsync()
|
||||
add({
|
||||
color: 'success',
|
||||
title: t('extension.remove.success.title', {
|
||||
extensionName: extensionToBeRemoved.value.name,
|
||||
}),
|
||||
description: t('extension.remove.success.text', {
|
||||
extensionName: extensionToBeRemoved.value.name,
|
||||
}),
|
||||
})
|
||||
await addNotificationAsync({
|
||||
text: t('extension.remove.success.text', {
|
||||
extensionName: extensionToBeRemoved.value.name,
|
||||
}),
|
||||
type: 'success',
|
||||
title: t('extension.remove.success.title', {
|
||||
extensionName: extensionToBeRemoved.value.name,
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
add({
|
||||
color: 'error',
|
||||
title: t('extension.remove.error.title'),
|
||||
description: t('extension.remove.error.text', {
|
||||
error: JSON.stringify(error),
|
||||
}),
|
||||
})
|
||||
await addNotificationAsync({
|
||||
type: 'error',
|
||||
title: t('extension.remove.error.title'),
|
||||
text: t('extension.remove.error.text', { error: JSON.stringify(error) }),
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
title: Erweiterungen
|
||||
subtitle: Entdecke und installiere Erweiterungen für HaexHub
|
||||
extension:
|
||||
installFromFile: Von Datei installieren
|
||||
add: Erweiterung hinzufügen
|
||||
success:
|
||||
title: '{extension} hinzugefügt'
|
||||
text: Die Erweiterung wurde erfolgreich hinzugefügt
|
||||
remove:
|
||||
success:
|
||||
text: 'Erweiterung {extensionName} wurde erfolgreich entfernt'
|
||||
title: '{extensionName} entfernt'
|
||||
error:
|
||||
text: "Erweiterung {extensionName} konnte nicht entfernt werden. \n {error}"
|
||||
title: 'Fehler beim Entfernen von {extensionName}'
|
||||
marketplace:
|
||||
comingSoon: Marketplace-Installation kommt bald!
|
||||
marketplace:
|
||||
official: Offizieller Marketplace
|
||||
community: Community Marketplace
|
||||
category:
|
||||
all: Alle
|
||||
productivity: Produktivität
|
||||
security: Sicherheit
|
||||
utilities: Werkzeuge
|
||||
integration: Integration
|
||||
search:
|
||||
placeholder: Erweiterungen durchsuchen...
|
||||
filter:
|
||||
category: Kategorie auswählen
|
||||
empty:
|
||||
title: Keine Erweiterungen gefunden
|
||||
description: Versuche einen anderen Suchbegriff oder eine andere Kategorie
|
||||
|
||||
en:
|
||||
title: Extensions
|
||||
subtitle: Discover and install extensions for HaexHub
|
||||
extension:
|
||||
installFromFile: Install from file
|
||||
add: Add Extension
|
||||
success:
|
||||
title: '{extension} added'
|
||||
text: Extension was added successfully
|
||||
remove:
|
||||
success:
|
||||
text: 'Extension {extensionName} was removed'
|
||||
title: '{extensionName} removed'
|
||||
error:
|
||||
text: "Extension {extensionName} couldn't be removed. \n {error}"
|
||||
title: 'Exception during uninstall {extensionName}'
|
||||
marketplace:
|
||||
comingSoon: Marketplace installation coming soon!
|
||||
marketplace:
|
||||
official: Official Marketplace
|
||||
community: Community Marketplace
|
||||
category:
|
||||
all: All
|
||||
productivity: Productivity
|
||||
security: Security
|
||||
utilities: Utilities
|
||||
integration: Integration
|
||||
search:
|
||||
placeholder: Search extensions...
|
||||
filter:
|
||||
category: Select category
|
||||
empty:
|
||||
title: No extensions found
|
||||
description: Try a different search term or category
|
||||
</i18n>
|
||||
@ -6,6 +6,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
name: 'vaultOverview',
|
||||
name: 'desktop',
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user