polyfill for spa added. works now on android

This commit is contained in:
2025-10-09 11:16:25 +02:00
parent c8c3a5c73f
commit fa3348a5ad
35 changed files with 2566 additions and 373 deletions

View File

@ -1,46 +1,49 @@
<template>
<div class="h-full flex flex-col">
<div class="h-screen w-screen flex flex-col">
<!-- Tab Bar -->
<div class="flex gap-2 p-2 bg-default overflow-x-auto border-b">
<div
<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="[
'btn btn-sm gap-2',
tabsStore.activeTabId === tab.extension.id
? 'btn-primary'
: 'btn-ghost',
'gap-2',
tabsStore.activeTabId === tab.extension.id ? 'primary' : 'neutral',
]"
@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>
<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>
</div>
<!-- IFrame Container -->
<div class="flex-1 relative overflow-hidden">
<div class="flex-1 relative min-h-0">
<div
v-for="tab in tabsStore.sortedTabs"
:key="tab.extension.id"
:style="{ display: tab.isVisible ? 'block' : 'none' }"
class="w-full h-full"
class="absolute inset-0"
>
<iframe
:ref="
(el) => registerIFrame(tab.extension.id, el as HTMLIFrameElement)
"
class="w-full h-full"
class="w-full h-full border-0"
:src="getExtensionUrl(tab.extension)"
sandbox="allow-scripts"
sandbox="allow-scripts allow-storage-access-by-user-activation allow-forms"
allow="autoplay; speaker-selection; encrypted-media;"
/>
</div>
@ -48,7 +51,7 @@
<!-- Loading State -->
<div
v-if="tabsStore.tabCount === 0"
class="flex items-center justify-center h-full"
class="absolute inset-0 flex items-center justify-center"
>
<p>{{ t('loading') }}</p>
</div>
@ -57,9 +60,14 @@
</template>
<script setup lang="ts">
import { useExtensionMessageHandler } from '~/composables/extensionMessageHandler'
import {
useExtensionMessageHandler,
registerExtensionIFrame,
unregisterExtensionIFrame,
} from '~/composables/extensionMessageHandler'
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
import type { IHaexHubExtension } from '~/types/haexhub'
import { platform } from '@tauri-apps/plugin-os'
definePageMeta({
name: 'haexExtension',
@ -79,43 +87,77 @@ watchEffect(() => {
}
})
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 },
)
// IFrame Registrierung und Message Handler Setup
/* const iframeRefs = new Map<string, HTMLIFrameElement>()
const setupMessageHandlers = new Set<string>() */
// 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)
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
if (!el) return
// Registriere IFrame im Store
tabsStore.registerIFrame(extensionId, el)
// Registriere IFrame im globalen Message Handler Registry
const tab = tabsStore.openTabs.get(extensionId)
if (tab?.extension) {
registerExtensionIFrame(el, tab.extension)
}
}
// 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 },
)
const os = await platform()
// Extension URL generieren
const getExtensionUrl = (extension: IHaexHubExtension) => {
const info = { id: extension.id, version: extension.version }
// Extract key_hash from full_extension_id (everything before first underscore)
const firstUnderscoreIndex = extension.id.indexOf('_')
if (firstUnderscoreIndex === -1) {
console.error('Invalid full_extension_id format:', extension.id)
return ''
}
const keyHash = extension.id.substring(0, firstUnderscoreIndex)
const info = {
key_hash: keyHash,
name: extension.name,
version: extension.version,
}
const jsonString = JSON.stringify(info)
const bytes = new TextEncoder().encode(jsonString)
const encoded = Array.from(bytes)
const encodedInfo = 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
// 'android', 'ios', 'windows' etc.
let schemeUrl: string
if (os === 'android' || os === 'windows') {
// Android/Windows: http://<scheme>.localhost/path
schemeUrl = `http://haex-extension.localhost/${encodedInfo}/index.html`
} else {
// macOS/Linux/iOS: Klassisch scheme://localhost/path
schemeUrl = `haex-extension://localhost/${encodedInfo}/index.html`
}
return schemeUrl
}
// Context Changes an alle Tabs broadcasten

View File

@ -1,48 +1,117 @@
<template>
<div class="flex flex-col p-4 relative h-full">
<!-- <div
v-if="extensionStore.availableExtensions.length"
class="flex"
<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"
>
<UiButton
class="fixed top-20 right-4"
@click="onSelectExtensionAsync"
<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"
>
<Icon
name="mdi:plus"
size="1.5em"
/>
</UiButton>
<HaexExtensionCard
v-for="_extension in extensionStore.availableExtensions"
v-bind="_extension"
:key="_extension.id"
@remove="onShowRemoveDialog(_extension)"
/>
</div> -->
{{ preview }}
<div class="h-full w-full">
<div class="fixed top-30 right-10">
<UiButton
:tooltip="t('extension.add')"
@click="onSelectExtensionAsync"
square
size="xl"
<!-- Marketplace Selector -->
<USelectMenu
v-model="selectedMarketplace"
:items="marketplaces"
value-key="id"
class="w-full sm:w-48"
>
<Icon
name="mdi:plus"
size="1.5em"
<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"
>
<template
v-for="ext in filteredExtensions"
:key="ext.id"
>
<!-- Installed Extension Card -->
<HaexExtensionInstalledCard
v-if="ext.isInstalled"
:extension="ext"
@open="navigateToExtension(ext.id)"
@settings="onShowExtensionSettings(ext)"
@remove="onShowRemoveDialog(ext)"
/>
</UiButton>
<!-- Marketplace Extension Card -->
<HaexExtensionMarketplaceCard
v-else
:extension="ext"
:is-installed="isExtensionInstalled(ext.id)"
@install="onInstallFromMarketplace(ext)"
@details="onShowExtensionDetails(ext)"
/>
</template>
</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"
:manifest="extension.manifest"
@confirm="addExtensionAsync"
v-model:preview="preview"
@confirm="reinstallExtensionAsync"
/>
<HaexExtensionDialogInstall
@ -110,6 +179,220 @@ 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([
{
id: 'haex-passy',
name: 'HaexPassDummy',
version: '1.0.0',
author: 'HaexHub Team',
description:
'Sicherer Passwort-Manager mit Ende-zu-Ende-Verschlüsselung und Autofill-Funktion.',
icon: 'i-heroicons-lock-closed',
downloads: 15420,
rating: 4.8,
verified: true,
tags: ['security', 'password', 'productivity'],
category: 'security',
downloadUrl: '/extensions/haex-pass-1.0.0.haextension',
},
{
id: 'haex-notes',
name: 'HaexNotes',
version: '2.1.0',
author: 'HaexHub Team',
description:
'Markdown-basierter Notizen-Editor mit Syntax-Highlighting und Live-Preview.',
icon: 'i-heroicons-document-text',
downloads: 8930,
rating: 4.5,
verified: true,
tags: ['productivity', 'notes', 'markdown'],
category: 'productivity',
downloadUrl: '/extensions/haex-notes-2.1.0.haextension',
},
{
id: 'haex-backup',
name: 'HaexBackup',
version: '1.5.2',
author: 'Community',
description:
'Automatische Backups deiner Daten mit Cloud-Sync-Unterstützung.',
icon: 'i-heroicons-cloud-arrow-up',
downloads: 5240,
rating: 4.6,
verified: false,
tags: ['backup', 'cloud', 'utilities'],
category: 'utilities',
downloadUrl: '/extensions/haex-backup-1.5.2.haextension',
},
{
id: 'haex-calendar',
name: 'HaexCalendar',
version: '3.0.1',
author: 'HaexHub Team',
description:
'Integrierter Kalender mit Event-Management und Synchronisation.',
icon: 'i-heroicons-calendar',
downloads: 12100,
rating: 4.7,
verified: true,
tags: ['productivity', 'calendar', 'events'],
category: 'productivity',
downloadUrl: '/extensions/haex-calendar-3.0.1.haextension',
},
{
id: 'haex-2fa',
name: 'Haex2FA',
version: '1.2.0',
author: 'Security Team',
description:
'2-Faktor-Authentifizierung Manager mit TOTP und Backup-Codes.',
icon: 'i-heroicons-shield-check',
downloads: 7800,
rating: 4.9,
verified: true,
tags: ['security', '2fa', 'authentication'],
category: 'security',
downloadUrl: '/extensions/haex-2fa-1.2.0.haextension',
},
{
id: 'haex-github',
name: 'GitHub Integration',
version: '1.0.5',
author: 'Community',
description:
'Direkter Zugriff auf GitHub Repositories, Issues und Pull Requests.',
icon: 'i-heroicons-code-bracket',
downloads: 4120,
rating: 4.3,
verified: false,
tags: ['integration', 'github', 'development'],
category: 'integration',
downloadUrl: '/extensions/haex-github-1.0.5.haextension',
},
])
// Combine installed extensions with marketplace extensions
const allExtensions = computed(() => {
// Map installed extensions to marketplace format
const installed = extensionStore.availableExtensions.map((ext) => ({
id: ext.id,
name: ext.name,
version: ext.version,
author: ext.author || 'Unknown',
description: 'Installed Extension',
icon: ext.icon || 'i-heroicons-puzzle-piece',
downloads: 0,
rating: 0,
verified: false,
tags: [],
category: 'utilities',
downloadUrl: '',
isInstalled: true,
}))
console.log('Installed extensions count:', installed.length)
console.log('All extensions:', [...installed, ...marketplaceExtensions.value])
// Merge with marketplace extensions
return [...installed, ...marketplaceExtensions.value]
})
// 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
})
})
// Check if extension is installed
const isExtensionInstalled = (extensionId: string) => {
return (
extensionStore.availableExtensions.some((ext) => ext.id === extensionId) ||
allExtensions.value.some((ext) => ext.id === extensionId)
)
}
// 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
}
// Navigate to installed extension
const router = useRouter()
const route = useRoute()
const localePath = useLocalePath()
const navigateToExtension = (extensionId: string) => {
router.push(
localePath({
name: 'haexExtension',
params: {
vaultId: route.params.vaultId,
extensionId,
},
}),
)
}
// Show extension settings
const onShowExtensionSettings = (ext: unknown) => {
console.log('Show settings:', ext)
// TODO: Show extension settings modal
}
// Show remove dialog
const onShowRemoveDialog = (ext: any) => {
extensionToBeRemoved.value = ext
showRemoveDialog.value = true
}
const onSelectExtensionAsync = async () => {
try {
extension.path = await open({ directory: false, recursive: true })
@ -119,11 +402,11 @@ const onSelectExtensionAsync = async () => {
if (!preview.value) return
// Check if already installed
const isAlreadyInstalled = await extensionStore.isExtensionInstalledAsync({
id: preview.value.manifest.id,
version: preview.value.manifest.version,
})
// Check if already installed using full_extension_id
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
const isAlreadyInstalled = extensionStore.availableExtensions.some(
ext => ext.id === fullExtensionId
)
if (isAlreadyInstalled) {
openOverwriteDialog.value = true
@ -138,7 +421,14 @@ const onSelectExtensionAsync = async () => {
const addExtensionAsync = async () => {
try {
await extensionStore.installAsync(extension.path)
console.log(
'preview.value?.editable_permissions',
preview.value?.editable_permissions,
)
await extensionStore.installAsync(
extension.path,
preview.value?.editable_permissions,
)
await extensionStore.loadExtensionsAsync()
add({
@ -162,16 +452,46 @@ const addExtensionAsync = async () => {
}
}
const showRemoveDialog = ref(false)
const extensionToBeRemoved = ref<IHaexHubExtension>()
const reinstallExtensionAsync = async () => {
try {
if (!preview.value) return
const onShowRemoveDialog = (extension: IHaexHubExtension) => {
extensionToBeRemoved.value = extension
showRemoveDialog.value = true
// Calculate full_extension_id
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
// Remove old extension first
await extensionStore.removeExtensionByFullIdAsync(fullExtensionId)
// 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?.id || !extensionToBeRemoved.value?.version) {
if (!extensionToBeRemoved.value?.id) {
add({
color: 'error',
description: 'Erweiterung kann nicht gelöscht werden',
@ -180,9 +500,9 @@ const removeExtensionAsync = async () => {
}
try {
await extensionStore.removeExtensionAsync(
// Use removeExtensionByFullIdAsync since ext.id is already the full_extension_id
await extensionStore.removeExtensionByFullIdAsync(
extensionToBeRemoved.value.id,
extensionToBeRemoved.value.version,
)
await extensionStore.loadExtensionsAsync()
add({
@ -222,8 +542,14 @@ const removeExtensionAsync = async () => {
<i18n lang="yaml">
de:
title: 'Erweiterung installieren'
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'
@ -231,14 +557,34 @@ de:
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
add: 'Erweiterung hinzufügen'
success:
title: '{extension} hinzugefügt'
text: 'Die Erweiterung wurde erfolgreich hinzugefügt'
en:
title: 'Install extension'
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'
@ -246,9 +592,22 @@ en:
error:
text: "Extension {extensionName} couldn't be removed. \n {error}"
title: 'Exception during uninstall {extensionName}'
add: 'Add Extension'
success:
title: '{extension} added'
text: 'Extensions was added successfully'
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>