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

@ -13,6 +13,12 @@ interface ExtensionRequest {
let globalHandlerRegistered = false
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
// Store context values that need to be accessed outside setup
let contextGetters: {
getTheme: () => string
getLocale: () => string
} | null = null
const registerGlobalMessageHandler = () => {
if (globalHandlerRegistered) return
@ -61,6 +67,8 @@ const registerGlobalMessageHandler = () => {
result = await handlePermissionsMethodAsync(request, extension)
} else if (request.method.startsWith('context.')) {
result = await handleContextMethodAsync(request)
} else if (request.method.startsWith('storage.')) {
result = await handleStorageMethodAsync(request, extension)
} else {
throw new Error(`Unknown method: ${request.method}`)
}
@ -96,6 +104,18 @@ export const useExtensionMessageHandler = (
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
extension: ComputedRef<IHaexHubExtension | undefined | null>,
) => {
// Initialize context getters (can use composables here because we're in setup)
const { currentTheme } = storeToRefs(useUiStore())
const { locale } = useI18n()
// Store getters for use outside setup context
if (!contextGetters) {
contextGetters = {
getTheme: () => currentTheme.value?.value || 'system',
getLocale: () => locale.value,
}
}
// Registriere globalen Handler beim ersten Aufruf
registerGlobalMessageHandler()
@ -114,6 +134,28 @@ export const useExtensionMessageHandler = (
})
}
// Export Funktion für manuelle IFrame-Registrierung (kein Composable!)
export const registerExtensionIFrame = (
iframe: HTMLIFrameElement,
extension: IHaexHubExtension,
) => {
// Stelle sicher, dass der globale Handler registriert ist
registerGlobalMessageHandler()
// Warnung wenn Context Getters nicht initialisiert wurden
if (!contextGetters) {
console.warn(
'Context getters not initialized. Make sure useExtensionMessageHandler was called in setup context first.',
)
}
iframeRegistry.set(iframe, extension)
}
export const unregisterExtensionIFrame = (iframe: HTMLIFrameElement) => {
iframeRegistry.delete(iframe)
}
// ==========================================
// Extension Methods
// ==========================================
@ -243,14 +285,16 @@ async function handlePermissionsMethodAsync(
// ==========================================
async function handleContextMethodAsync(request: ExtensionRequest) {
const { currentTheme } = storeToRefs(useUiStore())
const { locale } = useI18n()
switch (request.method) {
case 'context.get':
if (!contextGetters) {
throw new Error(
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
)
}
return {
theme: currentTheme.value || 'system',
locale: locale.value,
theme: contextGetters.getTheme(),
locale: contextGetters.getLocale(),
platform: detectPlatform(),
}
@ -265,3 +309,53 @@ function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
if (width < 1024) return 'tablet'
return 'desktop'
}
// ==========================================
// Storage Methods
// ==========================================
async function handleStorageMethodAsync(
request: ExtensionRequest,
extension: IHaexHubExtension,
) {
const storageKey = `ext_${extension.id}_`
console.log(`[HaexHub Storage] ${request.method} for extension ${extension.id}`)
switch (request.method) {
case 'storage.getItem': {
const key = request.params.key as string
return localStorage.getItem(storageKey + key)
}
case 'storage.setItem': {
const key = request.params.key as string
const value = request.params.value as string
localStorage.setItem(storageKey + key, value)
return null
}
case 'storage.removeItem': {
const key = request.params.key as string
localStorage.removeItem(storageKey + key)
return null
}
case 'storage.clear': {
// Remove only extension-specific keys
const keys = Object.keys(localStorage).filter(k => k.startsWith(storageKey))
keys.forEach(k => localStorage.removeItem(k))
return null
}
case 'storage.keys': {
// Return only extension-specific keys (without prefix)
const keys = Object.keys(localStorage)
.filter(k => k.startsWith(storageKey))
.map(k => k.substring(storageKey.length))
return keys
}
default:
throw new Error(`Unknown storage method: ${request.method}`)
}
}

View File

@ -0,0 +1,60 @@
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { platform } from '@tauri-apps/plugin-os'
import { getCurrentWindow } from '@tauri-apps/api/window'
/**
* Handles Android back button to navigate within the app instead of closing it
* Mimics browser behavior: navigate back if possible, close app if on first page
*/
export function useAndroidBackButton() {
const router = useRouter()
const historyStack = ref<string[]>([])
let unlisten: (() => void) | null = null
// Track navigation history manually
router.afterEach((to, from) => {
console.log('[AndroidBack] Navigation:', { to: to.path, from: from.path, stackSize: historyStack.value.length })
// If navigating forward (new page)
if (from.path && to.path !== from.path && !historyStack.value.includes(to.path)) {
historyStack.value.push(from.path)
console.log('[AndroidBack] Added to stack:', from.path, 'Stack:', historyStack.value)
}
})
onMounted(async () => {
const os = platform()
if (os === 'android') {
const appWindow = getCurrentWindow()
// Listen to close requested event (triggered by Android back button)
unlisten = await appWindow.onCloseRequested(async (event) => {
console.log('[AndroidBack] Back button pressed, stack size:', historyStack.value.length)
// Check if we have history
if (historyStack.value.length > 0) {
// Prevent window from closing
event.preventDefault()
// Remove current page from stack
historyStack.value.pop()
console.log('[AndroidBack] Going back, new stack size:', historyStack.value.length)
// Navigate back in router
router.back()
} else {
console.log('[AndroidBack] No history, allowing app to close')
}
// If no history, allow default behavior (app closes)
})
}
})
onUnmounted(() => {
if (unlisten) {
unlisten()
}
})
}