cleanup. renamed postMessgages

This commit is contained in:
2025-10-25 23:17:28 +02:00
parent 5fdea155d1
commit 86b65f117d
9 changed files with 176 additions and 340 deletions

View File

@ -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::<serde_json::Value>(&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 {

View File

@ -37,7 +37,7 @@ pub async fn get_all_extensions(
state: State<'_, AppState>,
) -> Result<Vec<ExtensionInfoResponse>, 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: <extension_path>/<haextension_dir>/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_<public_key_first_8>_<name>
let key_prefix = manifest
.public_key
.chars()
.take(8)
.collect::<String>();
let key_prefix = manifest.public_key.chars().take(8).collect::<String>();
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 })
}
}

View File

@ -85,7 +85,10 @@
>
<!-- Overview Mode: Teleport to window preview -->
<Teleport
v-if="windowManager.showWindowOverview && overviewWindowState.has(window.id)"
v-if="
windowManager.showWindowOverview &&
overviewWindowState.has(window.id)
"
:to="`#window-preview-${window.id}`"
>
<div
@ -330,9 +333,9 @@ const getWorkspaceIcons = (workspaceId: string) => {
.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<string, { x: number; y: number; width: number; height: number; scale: number }>(),
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, {

View File

@ -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({

View File

@ -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<Set<string>>(
'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,
}
}

View File

@ -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<string, unknown>
// 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<unknown[]>('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<string[]>('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))

View File

@ -90,6 +90,7 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
const extensions =
await invoke<ExtensionInfoResponse[]>('get_all_extensions')
console.log('get_all_extensions', extensions)
// ExtensionInfoResponse is now directly compatible with IHaexHubExtension
availableExtensions.value = extensions
} catch (error) {

View File

@ -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<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,
)
})
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,
}
})

View File

@ -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 {