diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index c53ebef..05db5b5 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -695,9 +695,10 @@ impl ExtensionManager { &extension_data.manifest.version, )?; - if !extension_path.exists() || !extension_path.join("manifest.json").exists() { + // Check if extension directory exists + if !extension_path.exists() { eprintln!( - "DEBUG: Extension files missing for: {} at {:?}", + "DEBUG: Extension directory missing for: {} at {:?}", extension_id, extension_path ); self.missing_extensions @@ -714,6 +715,71 @@ impl ExtensionManager { continue; } + // Read haextension_dir from config if it exists, otherwise use default + let config_path = extension_path.join("haextension.config.json"); + let haextension_dir = if config_path.exists() { + match std::fs::read_to_string(&config_path) { + Ok(config_content) => { + match serde_json::from_str::(&config_content) { + Ok(config) => { + let dir = config + .get("dev") + .and_then(|dev| dev.get("haextension_dir")) + .and_then(|dir| dir.as_str()) + .unwrap_or("haextension") + .to_string(); + + // Security: Validate that haextension_dir doesn't contain ".." + if dir.contains("..") { + eprintln!( + "DEBUG: Invalid haextension_dir for: {}, contains '..'", + extension_id + ); + self.missing_extensions + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })? + .push(MissingExtension { + id: extension_id.clone(), + public_key: extension_data.manifest.public_key.clone(), + name: extension_data.manifest.name.clone(), + version: extension_data.manifest.version.clone(), + }); + continue; + } + dir + } + Err(_) => "haextension".to_string(), + } + } + Err(_) => "haextension".to_string(), + } + } else { + "haextension".to_string() + }; + + // Check if manifest.json exists in the haextension_dir + let manifest_path = extension_path.join(&haextension_dir).join("manifest.json"); + if !manifest_path.exists() { + eprintln!( + "DEBUG: manifest.json missing for: {} at {:?}", + extension_id, manifest_path + ); + self.missing_extensions + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })? + .push(MissingExtension { + id: extension_id.clone(), + public_key: extension_data.manifest.public_key.clone(), + name: extension_data.manifest.name.clone(), + version: extension_data.manifest.version.clone(), + }); + continue; + } + eprintln!("DEBUG: Extension loaded successfully: {}", extension_id); let extension = Extension { diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index 23f77e9..9a05738 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -37,7 +37,7 @@ pub async fn get_all_extensions( state: State<'_, AppState>, ) -> Result, String> { // Check if extensions are loaded, if not load them first - let needs_loading = { + /* let needs_loading = { let prod_exts = state .extension_manager .production_extensions @@ -45,15 +45,15 @@ pub async fn get_all_extensions( .unwrap(); let dev_exts = state.extension_manager.dev_extensions.lock().unwrap(); prod_exts.is_empty() && dev_exts.is_empty() - }; + }; */ - if needs_loading { - state - .extension_manager - .load_installed_extensions(&app_handle, &state) - .await - .map_err(|e| format!("Failed to load extensions: {:?}", e))?; - } + /* if needs_loading { */ + state + .extension_manager + .load_installed_extensions(&app_handle, &state) + .await + .map_err(|e| format!("Failed to load extensions: {:?}", e))?; + /* } */ let mut extensions = Vec::new(); @@ -193,13 +193,7 @@ pub async fn remove_extension( ) -> Result<(), ExtensionError> { state .extension_manager - .remove_extension_internal( - &app_handle, - &public_key, - &name, - &version, - &state, - ) + .remove_extension_internal(&app_handle, &public_key, &name, &version, &state) .await } @@ -259,8 +253,8 @@ fn default_haextension_dir() -> String { /// Check if a dev server is reachable by making a simple HTTP request async fn check_dev_server_health(url: &str) -> bool { - use tauri_plugin_http::reqwest; use std::time::Duration; + use tauri_plugin_http::reqwest; // Try to connect with a short timeout let client = reqwest::Client::builder() @@ -295,17 +289,15 @@ pub async fn load_dev_extension( // 1. Read haextension.config.json to get dev server config and haextension directory let config_path = extension_path_buf.join("haextension.config.json"); let (host, port, haextension_dir) = if config_path.exists() { - let config_content = std::fs::read_to_string(&config_path).map_err(|e| { - ExtensionError::ValidationError { + let config_content = + std::fs::read_to_string(&config_path).map_err(|e| ExtensionError::ValidationError { reason: format!("Failed to read haextension.config.json: {}", e), - } - })?; + })?; - let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| { - ExtensionError::ValidationError { + let config: HaextensionConfig = + serde_json::from_str(&config_content).map_err(|e| ExtensionError::ValidationError { reason: format!("Failed to parse haextension.config.json: {}", e), - } - })?; + })?; (config.dev.host, config.dev.port, config.dev.haextension_dir) } else { @@ -329,7 +321,9 @@ pub async fn load_dev_extension( eprintln!("✅ Dev server is reachable"); // 2. Build path to manifest: //manifest.json - let manifest_path = extension_path_buf.join(&haextension_dir).join("manifest.json"); + let manifest_path = extension_path_buf + .join(&haextension_dir) + .join("manifest.json"); // Check if manifest exists if !manifest_path.exists() { @@ -342,20 +336,15 @@ pub async fn load_dev_extension( } // 3. Read and parse manifest - let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| { - ExtensionError::ManifestError { + let manifest_content = + std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError { reason: format!("Failed to read manifest: {}", e), - } - })?; + })?; let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?; // 4. Generate a unique ID for dev extension: dev__ - let key_prefix = manifest - .public_key - .chars() - .take(8) - .collect::(); + let key_prefix = manifest.public_key.chars().take(8).collect::(); let extension_id = format!("dev_{}_{}", key_prefix, manifest.name); // 5. Check if dev extension already exists (allow reload) @@ -404,13 +393,11 @@ pub fn remove_dev_extension( state: State<'_, AppState>, ) -> Result<(), ExtensionError> { // Only remove from dev_extensions, not production_extensions - let mut dev_exts = state - .extension_manager - .dev_extensions - .lock() - .map_err(|e| ExtensionError::MutexPoisoned { + let mut dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| { + ExtensionError::MutexPoisoned { reason: e.to_string(), - })?; + } + })?; // Find and remove by public_key and name let to_remove = dev_exts @@ -423,10 +410,7 @@ pub fn remove_dev_extension( eprintln!("✅ Dev extension removed: {}", name); Ok(()) } else { - Err(ExtensionError::NotFound { - public_key, - name, - }) + Err(ExtensionError::NotFound { public_key, name }) } } diff --git a/src/components/haex/desktop/index.vue b/src/components/haex/desktop/index.vue index fa878c5..46e5a4a 100644 --- a/src/components/haex/desktop/index.vue +++ b/src/components/haex/desktop/index.vue @@ -85,7 +85,10 @@ >
{ .filter((item) => item.workspaceId === workspaceId) .map((item) => { if (item.itemType === 'system') { - const systemWindow = windowManager.getAllSystemWindows().find( - (win) => win.id === item.referenceId, - ) + const systemWindow = windowManager + .getAllSystemWindows() + .find((win) => win.id === item.referenceId) return { ...item, @@ -346,6 +349,7 @@ const getWorkspaceIcons = (workspaceId: string) => { (ext) => ext.id === item.referenceId, ) + console.log('found ext', extension) return { ...item, label: extension?.name || 'Unknown', @@ -429,7 +433,9 @@ const handleDragOver = (event: DragEvent) => { const handleDrop = async (event: DragEvent, workspaceId: string) => { if (!event.dataTransfer) return - const launcherItemData = event.dataTransfer.getData('application/haex-launcher-item') + const launcherItemData = event.dataTransfer.getData( + 'application/haex-launcher-item', + ) if (!launcherItemData) return try { @@ -441,7 +447,9 @@ const handleDrop = async (event: DragEvent, workspaceId: string) => { } // Get drop position relative to desktop - const desktopRect = (event.currentTarget as HTMLElement).getBoundingClientRect() + const desktopRect = ( + event.currentTarget as HTMLElement + ).getBoundingClientRect() const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2) const y = Math.max(0, event.clientY - desktopRect.top - 32) @@ -451,7 +459,7 @@ const handleDrop = async (event: DragEvent, workspaceId: string) => { item.id, x, y, - workspaceId + workspaceId, ) } catch (error) { console.error('Failed to create desktop icon:', error) @@ -611,7 +619,10 @@ const MAX_PREVIEW_HEIGHT = 450 // 50% increase from 300 // Store window state for overview (position only, size stays original) const overviewWindowState = ref( - new Map(), + new Map< + string, + { x: number; y: number; width: number; height: number; scale: number } + >(), ) // Calculate scale and card dimensions for each window @@ -635,7 +646,10 @@ watch( finalScale = MIN_PREVIEW_WIDTH / window.width } if (scaledHeight < MIN_PREVIEW_HEIGHT) { - finalScale = Math.max(finalScale, MIN_PREVIEW_HEIGHT / window.height) + finalScale = Math.max( + finalScale, + MIN_PREVIEW_HEIGHT / window.height, + ) } overviewWindowState.value.set(window.id, { diff --git a/src/components/haex/extension/card.vue b/src/components/haex/extension/card.vue index 256f3cf..cc40a01 100644 --- a/src/components/haex/extension/card.vue +++ b/src/components/haex/extension/card.vue @@ -89,7 +89,11 @@ const removeExtensionAsync = async () => { } try { - await extensionStore.removeExtensionAsync(extension.id, extension.version) + await extensionStore.removeExtensionAsync( + extension.publicKey, + extension.name, + extension.version, + ) await extensionStore.loadExtensionsAsync() add({ diff --git a/src/composables/extensionContextBroadcast.ts b/src/composables/extensionContextBroadcast.ts deleted file mode 100644 index 1ce18eb..0000000 --- a/src/composables/extensionContextBroadcast.ts +++ /dev/null @@ -1,65 +0,0 @@ -// composables/extensionContextBroadcast.ts -// NOTE: This composable is deprecated. Use tabsStore.broadcastToAllTabs() instead. -// Keeping for backwards compatibility. - -import { getExtensionWindow } from './extensionMessageHandler' - -export const useExtensionContextBroadcast = () => { - // Globaler State für Extension IDs statt IFrames - const extensionIds = useState>( - 'extension-ids', - () => new Set(), - ) - - const registerExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => { - extensionIds.value.add(extensionId) - } - - const unregisterExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => { - extensionIds.value.delete(extensionId) - } - - const broadcastContextChange = (context: { - theme: string - locale: string - platform: string - }) => { - const message = { - type: 'context.changed', - data: { context }, - timestamp: Date.now(), - } - - extensionIds.value.forEach((extensionId) => { - const win = getExtensionWindow(extensionId) - if (win) { - win.postMessage(message, '*') - } - }) - } - - const broadcastSearchRequest = (query: string, requestId: string) => { - const message = { - type: 'search.request', - data: { - query: { query, limit: 10 }, - requestId, - }, - timestamp: Date.now(), - } - - extensionIds.value.forEach((extensionId) => { - const win = getExtensionWindow(extensionId) - if (win) { - win.postMessage(message, '*') - } - }) - } - - return { - registerExtensionIframe, - unregisterExtensionIframe, - broadcastContextChange, - broadcastSearchRequest, - } -} diff --git a/src/composables/extensionMessageHandler.ts b/src/composables/extensionMessageHandler.ts index ae604e8..1bd6836 100644 --- a/src/composables/extensionMessageHandler.ts +++ b/src/composables/extensionMessageHandler.ts @@ -166,20 +166,18 @@ const registerGlobalMessageHandler = () => { try { let result: unknown - if (request.method.startsWith('extension.')) { - result = await handleExtensionMethodAsync(request, instance.extension) - } else if (request.method.startsWith('db.')) { - result = await handleDatabaseMethodAsync(request, instance.extension) - } else if (request.method.startsWith('fs.')) { - result = await handleFilesystemMethodAsync(request, instance.extension) - } else if (request.method.startsWith('http.')) { - result = await handleHttpMethodAsync(request, instance.extension) - } else if (request.method.startsWith('permissions.')) { - result = await handlePermissionsMethodAsync(request, instance.extension) - } else if (request.method.startsWith('context.')) { + if (request.method.startsWith('haextension.context.')) { result = await handleContextMethodAsync(request) - } else if (request.method.startsWith('storage.')) { + } else if (request.method.startsWith('haextension.storage.')) { result = await handleStorageMethodAsync(request, instance) + } else if (request.method.startsWith('haextension.db.')) { + result = await handleDatabaseMethodAsync(request, instance.extension) + } else if (request.method.startsWith('haextension.fs.')) { + result = await handleFilesystemMethodAsync(request, instance.extension) + } else if (request.method.startsWith('haextension.http.')) { + result = await handleHttpMethodAsync(request, instance.extension) + } else if (request.method.startsWith('haextension.permissions.')) { + result = await handlePermissionsMethodAsync(request, instance.extension) } else { throw new Error(`Unknown method: ${request.method}`) } @@ -328,30 +326,27 @@ export const getExtensionWindow = (extensionId: string): Window | undefined => { return getAllInstanceWindows(extensionId)[0] } -// ========================================== -// Extension Methods -// ========================================== +// Broadcast context changes to all extension instances +export const broadcastContextToAllExtensions = (context: { + theme: string + locale: string + platform?: string +}) => { + const message = { + type: 'haextension.context.changed', + data: { context }, + timestamp: Date.now(), + } -async function handleExtensionMethodAsync( - request: ExtensionRequest, - extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr -) { - switch (request.method) { - case 'extension.getInfo': { - const info = (await invoke('get_extension_info', { - publicKey: extension.publicKey, - name: extension.name, - })) as Record - // Override allowedOrigin with the actual window origin - // This fixes the dev-mode issue where Rust returns "tauri://localhost" - // but the actual origin is "http://localhost:3003" - return { - ...info, - allowedOrigin: window.location.origin, - } + console.log('[ExtensionHandler] Broadcasting context to all extensions:', context) + + // Send to all registered extension windows + for (const [_, instance] of iframeRegistry.entries()) { + const win = windowIdToWindowMap.get(instance.windowId) + if (win) { + console.log('[ExtensionHandler] Sending context to:', instance.extension.name, instance.windowId) + win.postMessage(message, '*') } - default: - throw new Error(`Unknown extension method: ${request.method}`) } } @@ -369,7 +364,7 @@ async function handleDatabaseMethodAsync( } switch (request.method) { - case 'db.query': { + case 'haextension.db.query': { const rows = await invoke('extension_sql_select', { sql: params.query || '', params: params.params || [], @@ -383,7 +378,7 @@ async function handleDatabaseMethodAsync( } } - case 'db.execute': { + case 'haextension.db.execute': { await invoke('extension_sql_execute', { sql: params.query || '', params: params.params || [], @@ -397,7 +392,7 @@ async function handleDatabaseMethodAsync( } } - case 'db.transaction': { + case 'haextension.db.transaction': { const statements = (request.params as { statements?: string[] }).statements || [] @@ -467,7 +462,7 @@ async function handlePermissionsMethodAsync( async function handleContextMethodAsync(request: ExtensionRequest) { switch (request.method) { - case 'context.get': + case 'haextension.context.get': if (!contextGetters) { throw new Error( 'Context not initialized. Make sure useExtensionMessageHandler is called in a component.', @@ -499,25 +494,25 @@ async function handleStorageMethodAsync( ) switch (request.method) { - case 'storage.getItem': { + case 'haextension.storage.getItem': { const key = request.params.key as string return localStorage.getItem(storageKey + key) } - case 'storage.setItem': { + case 'haextension.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': { + case 'haextension.storage.removeItem': { const key = request.params.key as string localStorage.removeItem(storageKey + key) return null } - case 'storage.clear': { + case 'haextension.storage.clear': { // Remove only instance-specific keys const keys = Object.keys(localStorage).filter((k) => k.startsWith(storageKey), @@ -526,7 +521,7 @@ async function handleStorageMethodAsync( return null } - case 'storage.keys': { + case 'haextension.storage.keys': { // Return only instance-specific keys (without prefix) const keys = Object.keys(localStorage) .filter((k) => k.startsWith(storageKey)) diff --git a/src/stores/extensions/index.ts b/src/stores/extensions/index.ts index fef7585..4665301 100644 --- a/src/stores/extensions/index.ts +++ b/src/stores/extensions/index.ts @@ -90,6 +90,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => { const extensions = await invoke('get_all_extensions') + console.log('get_all_extensions', extensions) // ExtensionInfoResponse is now directly compatible with IHaexHubExtension availableExtensions.value = extensions } catch (error) { diff --git a/src/stores/extensions/tabs.ts b/src/stores/extensions/tabs.ts deleted file mode 100644 index dc02ec6..0000000 --- a/src/stores/extensions/tabs.ts +++ /dev/null @@ -1,175 +0,0 @@ -// stores/extensions/tabs.ts -import type { IHaexHubExtension } from '~/types/haexhub' -import { getExtensionWindow } from '~/composables/extensionMessageHandler' - -interface ExtensionTab { - extension: IHaexHubExtension - iframe: HTMLIFrameElement | null - isVisible: boolean - lastAccessed: number -} - -export const useExtensionTabsStore = defineStore('extensionTabsStore', () => { - // State - const openTabs = ref(new Map()) - const activeTabId = ref(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, - ) - }) - - const extensionsStore = useExtensionsStore() - - // Actions - const openTab = (extensionId: string) => { - const extension = extensionsStore.availableExtensions.find( - (ext) => ext.id === extensionId, - ) - - if (!extension) { - console.error(`Extension ${extensionId} nicht gefunden`) - return - } - - // Check if extension is enabled - if (!extension.enabled) { - console.warn( - `Extension ${extensionId} ist deaktiviert und kann nicht geöffnet werden`, - ) - 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) { - const now = Date.now() - const inactiveDuration = now - newTab.lastAccessed - const TEN_MINUTES = 10 * 60 * 1000 - - // Reload iframe if inactive for more than 10 minutes - if (inactiveDuration > TEN_MINUTES && newTab.iframe) { - console.log( - `[TabStore] Reloading extension ${extensionId} after ${Math.round(inactiveDuration / 1000)}s inactivity`, - ) - const currentSrc = newTab.iframe.src - newTab.iframe.src = 'about:blank' - // Small delay to ensure reload - setTimeout(() => { - if (newTab.iframe) { - newTab.iframe.src = currentSrc - } - }, 50) - } - - newTab.isVisible = true - newTab.lastAccessed = 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(({ extension }) => { - // Use sandbox-compatible window reference - const win = getExtensionWindow(extension.id) - if (win) { - win.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, - } -}) diff --git a/src/stores/ui/index.ts b/src/stores/ui/index.ts index 23d6323..ee57b2f 100644 --- a/src/stores/ui/index.ts +++ b/src/stores/ui/index.ts @@ -1,4 +1,5 @@ import { breakpointsTailwind } from '@vueuse/core' +import { broadcastContextToAllExtensions } from '~/composables/extensionMessageHandler' import de from './de.json' import en from './en.json' @@ -9,6 +10,8 @@ export const useUiStore = defineStore('uiStore', () => { const isSmallScreen = breakpoints.smaller('sm') const { $i18n } = useNuxtApp() + const { locale } = useI18n() + const { platform } = useDeviceStore() $i18n.setLocaleMessage('de', { ui: de, @@ -56,6 +59,15 @@ export const useUiStore = defineStore('uiStore', () => { colorMode.preference = currentThemeName.value }) + // Broadcast theme and locale changes to extensions + watch([currentThemeName, locale], () => { + broadcastContextToAllExtensions({ + theme: currentThemeName.value, + locale: locale.value, + platform, + }) + }) + const viewportHeightWithoutHeader = ref(0) return {