mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
extensions fixed
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,4 +25,5 @@ dist-ssr
|
||||
.nuxt
|
||||
src-tauri/target
|
||||
nogit*
|
||||
.claude
|
||||
.claude
|
||||
.output
|
||||
@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ExtensionInfoResponse = { keyHash: string, name: string, fullId: string, version: string, displayName: string | null, namespace: string | null, allowedOrigin: string, };
|
||||
export type ExtensionInfoResponse = { keyHash: string, name: string, fullId: string, version: string, displayName: string | null, namespace: string | null, allowedOrigin: string, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, };
|
||||
|
||||
@ -196,6 +196,10 @@ pub struct ExtensionInfoResponse {
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
pub enabled: bool,
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
@ -204,11 +208,7 @@ impl ExtensionInfoResponse {
|
||||
) -> Result<Self, ExtensionError> {
|
||||
use crate::extension::core::types::get_tauri_origin;
|
||||
|
||||
// In development mode, use a wildcard for localhost to match any port
|
||||
#[cfg(debug_assertions)]
|
||||
let allowed_origin = "http://localhost:3003".to_string();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
// Always use the current Tauri origin to support all platforms (Desktop, Android, iOS)
|
||||
let allowed_origin = get_tauri_origin();
|
||||
|
||||
let key_hash = extension.manifest.calculate_key_hash()?;
|
||||
@ -222,6 +222,10 @@ impl ExtensionInfoResponse {
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
enabled: extension.enabled,
|
||||
description: extension.manifest.description.clone(),
|
||||
homepage: extension.manifest.homepage.clone(),
|
||||
icon: extension.manifest.icon.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,275 +260,6 @@ pub fn extension_protocol_handler(
|
||||
println!("Path: {}", path_str);
|
||||
println!("Asset to load: {}", asset_to_load);
|
||||
|
||||
/* match process_hex_encoded_json(&encoded_info) {
|
||||
Ok(info) => {
|
||||
println!("=== Extension Protocol Handler ===");
|
||||
println!("Full URI: {}", uri_ref);
|
||||
println!("Origin: {}", origin);
|
||||
println!("Encoded Info (aus Origin): {}", encoded_info);
|
||||
println!("Path: {}", path_str);
|
||||
println!("Asset to load: {}", asset_to_load);
|
||||
println!("Decoded info:");
|
||||
println!(" KeyHash: {}", info.key_hash);
|
||||
println!(" Name: {}", info.name);
|
||||
println!(" Version: {}", info.version);
|
||||
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
state,
|
||||
&info.key_hash,
|
||||
&info.name,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
|
||||
println!("Resolved path: {}", absolute_secure_path.display());
|
||||
println!("File exists: {}", absolute_secure_path.exists());
|
||||
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(mut content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
|
||||
if asset_to_load == "index.html" && mime_type.contains("html") {
|
||||
if let Ok(html_str) = String::from_utf8(content.clone()) {
|
||||
let base_tag = format!(r#"<base href="/{}/">"#, encoded_info);
|
||||
let modified_html = if let Some(head_pos) = html_str.find("<head>")
|
||||
{
|
||||
let insert_pos = head_pos + 6;
|
||||
format!(
|
||||
"{}{}<head>{}",
|
||||
&html_str[..insert_pos],
|
||||
base_tag,
|
||||
&html_str[insert_pos..]
|
||||
)
|
||||
} else {
|
||||
// Fallback: Prepend
|
||||
format!("{}{}", base_tag, html_str)
|
||||
};
|
||||
content = modified_html.into_bytes();
|
||||
}
|
||||
}
|
||||
// Inject localStorage polyfill for HTML files with caching
|
||||
if asset_to_load == "index.html" && mime_type.contains("html") {
|
||||
// Create cache key: extension_id (from host)
|
||||
let cache_key = format!("{}_{}", host, asset_to_load);
|
||||
|
||||
// Check cache first
|
||||
if let Ok(cache) = HTML_CACHE.lock() {
|
||||
if let Some(cached_content) = cache.get(&cache_key) {
|
||||
println!("Serving cached HTML for: {}", cache_key);
|
||||
content = cached_content.clone();
|
||||
|
||||
let content_length = content.len();
|
||||
return Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.header("X-HaexHub-Cache", "HIT")
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, OPTIONS",
|
||||
)
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.header("Access-Control-Allow-Credentials", "true")
|
||||
.body(content)
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Not in cache, modify and cache it
|
||||
if let Ok(html_content) = String::from_utf8(content.clone()) {
|
||||
let polyfill = r#"<script>
|
||||
// HaexHub localStorage polyfill for custom protocol
|
||||
(function() {
|
||||
try {
|
||||
// Test if localStorage is available
|
||||
let localStorageAvailable = false;
|
||||
try {
|
||||
window.localStorage.setItem('__test__', '1');
|
||||
window.localStorage.removeItem('__test__');
|
||||
localStorageAvailable = true;
|
||||
} catch (e) {
|
||||
// localStorage is blocked
|
||||
}
|
||||
|
||||
if (!localStorageAvailable) {
|
||||
// Create in-memory storage fallback
|
||||
const storage = new Map();
|
||||
const storagePolyfill = {
|
||||
getItem: (key) => storage.get(key) ?? null,
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
removeItem: (key) => storage.delete(key),
|
||||
clear: () => storage.clear(),
|
||||
get length() { return storage.size; },
|
||||
key: (index) => Array.from(storage.keys())[index] ?? null,
|
||||
};
|
||||
|
||||
// Try to replace localStorage
|
||||
try {
|
||||
delete window.localStorage;
|
||||
window.localStorage = storagePolyfill;
|
||||
} catch (e) {
|
||||
// On some platforms, we can't delete localStorage
|
||||
// Try to override methods instead
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: storagePolyfill,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
// Also replace sessionStorage
|
||||
try {
|
||||
delete window.sessionStorage;
|
||||
window.sessionStorage = {
|
||||
getItem: (key) => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
get length() { return 0; },
|
||||
key: () => null,
|
||||
};
|
||||
} catch (e) {
|
||||
// sessionStorage replacement failed, not critical
|
||||
}
|
||||
|
||||
console.log('[HaexHub] localStorage replaced with in-memory polyfill');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[HaexHub] Polyfill initialization failed:', e);
|
||||
}
|
||||
})();
|
||||
</script>"#;
|
||||
|
||||
// Inject as the FIRST thing in <head>, before any other script
|
||||
let modified_html = if let Some(head_pos) =
|
||||
html_content.find("<head>")
|
||||
{
|
||||
let insert_pos = head_pos + 6; // After <head>
|
||||
format!(
|
||||
"{}{}{}",
|
||||
&html_content[..insert_pos],
|
||||
polyfill,
|
||||
&html_content[insert_pos..]
|
||||
)
|
||||
} else if let Some(charset_pos) = html_content.find("<meta charset")
|
||||
{
|
||||
// Insert before first meta tag
|
||||
format!(
|
||||
"{}<head>{}</head>{}",
|
||||
&html_content[..charset_pos],
|
||||
polyfill,
|
||||
&html_content[charset_pos..]
|
||||
)
|
||||
} else if let Some(html_pos) = html_content.find("<!DOCTYPE html>")
|
||||
{
|
||||
// Insert right after DOCTYPE
|
||||
let insert_pos = html_pos + 15; // After <!DOCTYPE html>
|
||||
format!(
|
||||
"{}<head>{}</head>{}",
|
||||
&html_content[..insert_pos],
|
||||
polyfill,
|
||||
&html_content[insert_pos..]
|
||||
)
|
||||
} else {
|
||||
// Prepend to entire file
|
||||
format!("{}{}", polyfill, html_content)
|
||||
};
|
||||
|
||||
content = modified_html.into_bytes();
|
||||
|
||||
// Cache the modified HTML
|
||||
if let Ok(mut cache) = HTML_CACHE.lock() {
|
||||
cache.insert(cache_key, content.clone());
|
||||
println!("Cached modified HTML for future requests");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ",
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", &mime_type)
|
||||
.header("Content-Length", content_length.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.header(
|
||||
"X-HaexHub-Cache",
|
||||
if asset_to_load == "index.html" && mime_type.contains("html") {
|
||||
"MISS"
|
||||
} else {
|
||||
"N/A"
|
||||
},
|
||||
)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.header("Access-Control-Allow-Credentials", "true")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
Response::builder()
|
||||
.status(404)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
&state,
|
||||
|
||||
@ -47,7 +47,9 @@ pub fn get_tauri_origin() -> String {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
// On Android, with http://*.localhost URLs, the origin is "null"
|
||||
// This is a browser security feature for local/file protocols
|
||||
"null".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
|
||||
123
src/components/haex/extension/launcher.vue
Normal file
123
src/components/haex/extension/launcher.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<UPopover v-model:open="open">
|
||||
<UButton
|
||||
icon="material-symbols:apps"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
v-bind="$attrs"
|
||||
size="xl"
|
||||
/>
|
||||
|
||||
<template #content>
|
||||
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
||||
<!-- Enabled Extensions -->
|
||||
<UiButton
|
||||
v-for="extension in enabledExtensions"
|
||||
:key="extension.id"
|
||||
square
|
||||
size="xl"
|
||||
variant="ghost"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
|
||||
:label="extension.name"
|
||||
:tooltip="extension.name"
|
||||
@click="openExtension(extension.id)"
|
||||
/>
|
||||
|
||||
<!-- Disabled Extensions (grayed out) -->
|
||||
<UiButton
|
||||
v-for="extension in disabledExtensions"
|
||||
:key="extension.id"
|
||||
square
|
||||
size="xl"
|
||||
variant="ghost"
|
||||
:disabled="true"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
|
||||
:label="extension.name"
|
||||
:tooltip="`${extension.name} (${t('disabled')})`"
|
||||
/>
|
||||
|
||||
<!-- Marketplace Button (always at the end) -->
|
||||
<UiButton
|
||||
square
|
||||
size="xl"
|
||||
variant="soft"
|
||||
color="primary"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
icon="i-heroicons-plus-circle"
|
||||
:label="t('marketplace')"
|
||||
:tooltip="t('marketplace')"
|
||||
@click="openMarketplace"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const extensionStore = useExtensionsStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const localePath = useLocalePath()
|
||||
const { t } = useI18n()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
// Enabled extensions first
|
||||
const enabledExtensions = computed(() => {
|
||||
return extensionStore.availableExtensions.filter((ext) => ext.enabled)
|
||||
})
|
||||
|
||||
// Disabled extensions last
|
||||
const disabledExtensions = computed(() => {
|
||||
return extensionStore.availableExtensions.filter((ext) => !ext.enabled)
|
||||
})
|
||||
|
||||
const openExtension = (extensionId: string) => {
|
||||
router.push(
|
||||
localePath({
|
||||
name: 'haexExtension',
|
||||
params: {
|
||||
vaultId: route.params.vaultId,
|
||||
extensionId,
|
||||
},
|
||||
}),
|
||||
)
|
||||
open.value = false
|
||||
}
|
||||
|
||||
const openMarketplace = () => {
|
||||
router.push(
|
||||
localePath({
|
||||
name: 'extensionOverview',
|
||||
params: {
|
||||
vaultId: route.params.vaultId,
|
||||
},
|
||||
}),
|
||||
)
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
disabled: Deaktiviert
|
||||
marketplace: Marketplace
|
||||
|
||||
en:
|
||||
disabled: Disabled
|
||||
marketplace: Marketplace
|
||||
</i18n>
|
||||
@ -57,10 +57,17 @@
|
||||
{{ extension.description }}
|
||||
</p>
|
||||
|
||||
<!-- Stats -->
|
||||
<!-- Stats and Status -->
|
||||
<div
|
||||
class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<div
|
||||
v-if="isInstalled"
|
||||
class="flex items-center gap-1 text-success font-medium"
|
||||
>
|
||||
<UIcon name="i-heroicons-check-circle-solid" />
|
||||
<span>{{ t('installed') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="extension.downloads"
|
||||
class="flex items-center gap-1"
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<UPopover v-model:open="open">
|
||||
<UButton
|
||||
icon="material-symbols:apps"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
v-bind="$attrs"
|
||||
size="xl"
|
||||
/>
|
||||
|
||||
<template #content>
|
||||
<ul
|
||||
class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll"
|
||||
@click="open = false"
|
||||
>
|
||||
<UiButton
|
||||
v-for="item in menu"
|
||||
:key="item.id"
|
||||
square
|
||||
size="xl"
|
||||
variant="ghost"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
:icon="item.icon"
|
||||
:label="item.name"
|
||||
:tooltip="item.name"
|
||||
@click="item.onSelect"
|
||||
/>
|
||||
<!-- <UiButton
|
||||
v-for="item in extensionLinks"
|
||||
:key="item.id"
|
||||
v-bind="item"
|
||||
icon-type="svg"
|
||||
/> -->
|
||||
</ul>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//const { extensionLinks } = storeToRefs(useExtensionsStore())
|
||||
const { menu } = storeToRefs(useSidebarStore())
|
||||
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
settings: 'Einstellungen'
|
||||
close: 'Vault schließen'
|
||||
|
||||
en:
|
||||
settings: 'Settings'
|
||||
close: 'Close Vault'
|
||||
</i18n>
|
||||
@ -1,17 +1,22 @@
|
||||
// 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 alle aktiven IFrames
|
||||
const extensionIframes = useState<Set<HTMLIFrameElement>>(
|
||||
'extension-iframes',
|
||||
// Globaler State für Extension IDs statt IFrames
|
||||
const extensionIds = useState<Set<string>>(
|
||||
'extension-ids',
|
||||
() => new Set(),
|
||||
)
|
||||
|
||||
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value.add(iframe)
|
||||
const registerExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
||||
extensionIds.value.add(extensionId)
|
||||
}
|
||||
|
||||
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
||||
extensionIframes.value.delete(iframe)
|
||||
const unregisterExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
||||
extensionIds.value.delete(extensionId)
|
||||
}
|
||||
|
||||
const broadcastContextChange = (context: {
|
||||
@ -25,8 +30,11 @@ export const useExtensionContextBroadcast = () => {
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
extensionIframes.value.forEach((iframe) => {
|
||||
iframe.contentWindow?.postMessage(message, '*')
|
||||
extensionIds.value.forEach((extensionId) => {
|
||||
const win = getExtensionWindow(extensionId)
|
||||
if (win) {
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -40,8 +48,11 @@ export const useExtensionContextBroadcast = () => {
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
extensionIframes.value.forEach((iframe) => {
|
||||
iframe.contentWindow?.postMessage(message, '*')
|
||||
extensionIds.value.forEach((extensionId) => {
|
||||
const win = getExtensionWindow(extensionId)
|
||||
if (win) {
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,10 @@ interface ExtensionRequest {
|
||||
// Globaler Handler - nur einmal registriert
|
||||
let globalHandlerRegistered = false
|
||||
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
|
||||
// Map event.source (WindowProxy) to extension for sandbox-compatible matching
|
||||
const sourceRegistry = new Map<Window, IHaexHubExtension>()
|
||||
// Reverse map: extension ID to Window for broadcasting
|
||||
const extensionToWindowMap = new Map<string, Window>()
|
||||
|
||||
// Store context values that need to be accessed outside setup
|
||||
let contextGetters: {
|
||||
@ -22,36 +26,44 @@ let contextGetters: {
|
||||
const registerGlobalMessageHandler = () => {
|
||||
if (globalHandlerRegistered) return
|
||||
|
||||
window.addEventListener('message', async (event: MessageEvent) => {
|
||||
// Finde die Extension für dieses IFrame
|
||||
let extension: IHaexHubExtension | undefined
|
||||
let sourceIframe: HTMLIFrameElement | undefined
|
||||
console.log('[Parent Debug] ⭐ NEW VERSION LOADED - NO MORE CONTENTWINDOW ⭐')
|
||||
|
||||
for (const [iframe, ext] of iframeRegistry.entries()) {
|
||||
if (event.source === iframe.contentWindow) {
|
||||
extension = ext
|
||||
sourceIframe = iframe
|
||||
break
|
||||
}
|
||||
window.addEventListener('message', async (event: MessageEvent) => {
|
||||
// Ignore console.forward messages - they're handled elsewhere
|
||||
if (event.data?.type === 'console.forward') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!extension || !sourceIframe) {
|
||||
// Finde die Extension für dieses event.source (sandbox-compatible)
|
||||
let extension = sourceRegistry.get(event.source as Window)
|
||||
|
||||
// If not registered yet, register on first message from this source
|
||||
if (!extension && iframeRegistry.size === 1) {
|
||||
// If we only have one iframe, assume this message is from it
|
||||
const entry = Array.from(iframeRegistry.entries())[0]
|
||||
if (entry) {
|
||||
const [_, ext] = entry
|
||||
const windowSource = event.source as Window
|
||||
sourceRegistry.set(windowSource, ext)
|
||||
extensionToWindowMap.set(ext.id, windowSource)
|
||||
extension = ext
|
||||
}
|
||||
} else if (extension && !extensionToWindowMap.has(extension.id)) {
|
||||
// Also register in reverse map for broadcasting
|
||||
extensionToWindowMap.set(extension.id, event.source as Window)
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
return // Message ist nicht von einem registrierten IFrame
|
||||
}
|
||||
|
||||
const request = event.data as ExtensionRequest
|
||||
|
||||
if (!request.id || !request.method) {
|
||||
console.error('Invalid extension request:', request)
|
||||
console.error('[ExtensionHandler] Invalid extension request:', request)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[HaexHub] ${extension.name} request:`,
|
||||
request.method,
|
||||
request.params,
|
||||
)
|
||||
|
||||
try {
|
||||
let result: unknown
|
||||
|
||||
@ -73,17 +85,25 @@ const registerGlobalMessageHandler = () => {
|
||||
throw new Error(`Unknown method: ${request.method}`)
|
||||
}
|
||||
|
||||
sourceIframe.contentWindow?.postMessage(
|
||||
// Use event.source instead of contentWindow to work with sandboxed iframes
|
||||
// For sandboxed iframes, event.origin is "null" (string), which is not valid for postMessage
|
||||
const targetOrigin = event.origin === 'null' ? '*' : (event.origin || '*')
|
||||
|
||||
;(event.source as Window)?.postMessage(
|
||||
{
|
||||
id: request.id,
|
||||
result,
|
||||
},
|
||||
'*',
|
||||
targetOrigin,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[HaexHub] Extension request error:', error)
|
||||
console.error('[ExtensionHandler] Extension request error:', error)
|
||||
|
||||
sourceIframe.contentWindow?.postMessage(
|
||||
// Use event.source instead of contentWindow to work with sandboxed iframes
|
||||
// For sandboxed iframes, event.origin is "null" (string), which is not valid for postMessage
|
||||
const targetOrigin = event.origin === 'null' ? '*' : (event.origin || '*')
|
||||
|
||||
;(event.source as Window)?.postMessage(
|
||||
{
|
||||
id: request.id,
|
||||
error: {
|
||||
@ -92,7 +112,7 @@ const registerGlobalMessageHandler = () => {
|
||||
details: error,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
targetOrigin,
|
||||
)
|
||||
}
|
||||
})
|
||||
@ -153,9 +173,26 @@ export const registerExtensionIFrame = (
|
||||
}
|
||||
|
||||
export const unregisterExtensionIFrame = (iframe: HTMLIFrameElement) => {
|
||||
// Also remove from source registry
|
||||
const ext = iframeRegistry.get(iframe)
|
||||
if (ext) {
|
||||
// Find and remove all sources pointing to this extension
|
||||
for (const [source, extension] of sourceRegistry.entries()) {
|
||||
if (extension === ext) {
|
||||
sourceRegistry.delete(source)
|
||||
}
|
||||
}
|
||||
// Remove from extension-to-window map
|
||||
extensionToWindowMap.delete(ext.id)
|
||||
}
|
||||
iframeRegistry.delete(iframe)
|
||||
}
|
||||
|
||||
// Export function to get Window for an extension (for broadcasting)
|
||||
export const getExtensionWindow = (extensionId: string): Window | undefined => {
|
||||
return extensionToWindowMap.get(extensionId)
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Extension Methods
|
||||
// ==========================================
|
||||
@ -165,10 +202,18 @@ async function handleExtensionMethodAsync(
|
||||
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
||||
) {
|
||||
switch (request.method) {
|
||||
case 'extension.getInfo':
|
||||
return await invoke('get_extension_info', {
|
||||
case 'extension.getInfo': {
|
||||
const info = await invoke('get_extension_info', {
|
||||
extensionId: extension.id,
|
||||
})
|
||||
}) 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,
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown extension method: ${request.method}`)
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<HaexMenuApplications :block="isSmallScreen" />
|
||||
<HaexExtensionLauncher :block="isSmallScreen" />
|
||||
<UiDropdownVault :block="isSmallScreen" />
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
@ -27,14 +27,34 @@
|
||||
</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 ? 'block' : 'none' }"
|
||||
:style="{ display: tab.isVisible && !showConsole ? 'block' : 'none' }"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<iframe
|
||||
@ -48,6 +68,81 @@
|
||||
/>
|
||||
</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"
|
||||
@ -68,7 +163,10 @@ import {
|
||||
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { EXTENSION_PROTOCOL_NAME, EXTENSION_PROTOCOL_PREFIX } from '~/config/constants'
|
||||
import {
|
||||
EXTENSION_PROTOCOL_NAME,
|
||||
EXTENSION_PROTOCOL_PREFIX,
|
||||
} from '~/config/constants'
|
||||
|
||||
definePageMeta({
|
||||
name: 'haexExtension',
|
||||
@ -78,6 +176,23 @@ const { t } = useI18n()
|
||||
|
||||
const tabsStore = useExtensionTabsStore()
|
||||
|
||||
// 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)
|
||||
|
||||
@ -94,19 +209,71 @@ 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,
|
||||
@ -184,11 +351,16 @@ watch([currentTheme, locale], () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup beim Verlassen
|
||||
onBeforeUnmount(() => {
|
||||
// Optional: Alle Tabs schließen oder offen lassen
|
||||
// tabsStore.closeAllTabs()
|
||||
})
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<i18n lang="yaml">
|
||||
|
||||
@ -67,27 +67,15 @@
|
||||
v-if="filteredExtensions.length"
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
<template
|
||||
<!-- Marketplace Extension Card -->
|
||||
<HaexExtensionMarketplaceCard
|
||||
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)"
|
||||
/>
|
||||
<!-- Marketplace Extension Card -->
|
||||
<HaexExtensionMarketplaceCard
|
||||
v-else
|
||||
:extension="ext"
|
||||
:is-installed="isExtensionInstalled(ext.id)"
|
||||
@install="onInstallFromMarketplace(ext)"
|
||||
@details="onShowExtensionDetails(ext)"
|
||||
/>
|
||||
</template>
|
||||
:extension="ext"
|
||||
:is-installed="ext.isInstalled"
|
||||
@install="onInstallFromMarketplace(ext)"
|
||||
@details="onShowExtensionDetails(ext)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
@ -132,6 +120,7 @@
|
||||
import type {
|
||||
IHaexHubExtension,
|
||||
IHaexHubExtensionManifest,
|
||||
IMarketplaceExtension,
|
||||
} from '~/types/haexhub'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
|
||||
@ -208,7 +197,7 @@ const categories = computed(() => [
|
||||
])
|
||||
|
||||
// Dummy Marketplace Extensions (später von API laden)
|
||||
const marketplaceExtensions = ref([
|
||||
const marketplaceExtensions = ref<IMarketplaceExtension[]>([
|
||||
{
|
||||
id: 'haex-passy',
|
||||
name: 'HaexPassDummy',
|
||||
@ -217,12 +206,14 @@ const marketplaceExtensions = ref([
|
||||
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',
|
||||
@ -232,12 +223,14 @@ const marketplaceExtensions = ref([
|
||||
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',
|
||||
@ -247,12 +240,14 @@ const marketplaceExtensions = ref([
|
||||
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',
|
||||
@ -262,12 +257,14 @@ const marketplaceExtensions = ref([
|
||||
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',
|
||||
@ -277,12 +274,14 @@ const marketplaceExtensions = ref([
|
||||
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',
|
||||
@ -292,39 +291,26 @@ const marketplaceExtensions = ref([
|
||||
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,
|
||||
},
|
||||
])
|
||||
|
||||
// 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,
|
||||
// Mark marketplace extensions as installed if they exist in availableExtensions
|
||||
const allExtensions = computed((): IMarketplaceExtension[] => {
|
||||
return marketplaceExtensions.value.map((ext) => ({
|
||||
...ext,
|
||||
// Check if this marketplace extension is already installed
|
||||
isInstalled: extensionStore.availableExtensions.some(
|
||||
(installed) => installed.name === ext.name,
|
||||
),
|
||||
}))
|
||||
|
||||
console.log('Installed extensions count:', installed.length)
|
||||
console.log('All extensions:', [...installed, ...marketplaceExtensions.value])
|
||||
|
||||
// Merge with marketplace extensions
|
||||
return [...installed, ...marketplaceExtensions.value]
|
||||
})
|
||||
|
||||
// Filtered Extensions
|
||||
@ -333,7 +319,7 @@ const filteredExtensions = computed(() => {
|
||||
const matchesSearch =
|
||||
!searchQuery.value ||
|
||||
ext.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
ext.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
ext.description?.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
|
||||
const matchesCategory =
|
||||
selectedCategory.value === 'all' ||
|
||||
@ -343,14 +329,6 @@ const filteredExtensions = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 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)
|
||||
@ -364,35 +342,6 @@ const onShowExtensionDetails = (ext: unknown) => {
|
||||
// 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 })
|
||||
@ -405,7 +354,7 @@ const onSelectExtensionAsync = async () => {
|
||||
// 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
|
||||
(ext) => ext.id === fullExtensionId,
|
||||
)
|
||||
|
||||
if (isAlreadyInstalled) {
|
||||
|
||||
77
src/plugins/console-interceptor.ts
Normal file
77
src/plugins/console-interceptor.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Global Console Interceptor Plugin
|
||||
* Captures all console messages app-wide for debugging
|
||||
*/
|
||||
|
||||
export interface ConsoleLog {
|
||||
timestamp: string
|
||||
level: 'log' | 'info' | 'warn' | 'error' | 'debug'
|
||||
message: string
|
||||
}
|
||||
|
||||
// Global storage for console logs
|
||||
export const globalConsoleLogs = ref<ConsoleLog[]>([])
|
||||
|
||||
// Store original console methods
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
debug: console.debug,
|
||||
}
|
||||
|
||||
function interceptConsole(level: 'log' | 'info' | 'warn' | 'error' | 'debug') {
|
||||
console[level] = function (...args: unknown[]) {
|
||||
// Call original console method
|
||||
originalConsole[level].apply(console, args)
|
||||
|
||||
// Add to global log display
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
const message = args
|
||||
.map((arg) => {
|
||||
if (arg === null) return 'null'
|
||||
if (arg === undefined) return 'undefined'
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2)
|
||||
} catch {
|
||||
return String(arg)
|
||||
}
|
||||
}
|
||||
return String(arg)
|
||||
})
|
||||
.join(' ')
|
||||
|
||||
globalConsoleLogs.value.push({
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
})
|
||||
|
||||
// Limit to last 1000 logs
|
||||
if (globalConsoleLogs.value.length > 1000) {
|
||||
globalConsoleLogs.value = globalConsoleLogs.value.slice(-1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Install console interceptors immediately
|
||||
interceptConsole('log')
|
||||
interceptConsole('info')
|
||||
interceptConsole('warn')
|
||||
interceptConsole('error')
|
||||
interceptConsole('debug')
|
||||
|
||||
console.log('[HaexHub] Global console interceptor installed')
|
||||
|
||||
return {
|
||||
provide: {
|
||||
consoleLogs: globalConsoleLogs,
|
||||
clearConsoleLogs: () => {
|
||||
globalConsoleLogs.value = []
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -8,16 +8,7 @@ import type {
|
||||
} from '~/types/haexhub'
|
||||
import type { ExtensionPreview } from '@bindings/ExtensionPreview'
|
||||
import type { ExtensionPermissions } from '~~/src-tauri/bindings/ExtensionPermissions'
|
||||
|
||||
interface ExtensionInfoResponse {
|
||||
keyHash: string
|
||||
name: string
|
||||
fullId: string
|
||||
version: string
|
||||
displayName: string | null
|
||||
namespace: string | null
|
||||
allowedOrigin: string
|
||||
}
|
||||
import type { ExtensionInfoResponse } from '~~/src-tauri/bindings/ExtensionInfoResponse'
|
||||
|
||||
/* const manifestFileName = 'manifest.json'
|
||||
const logoFileName = 'icon.svg' */
|
||||
@ -130,8 +121,10 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
name: ext.displayName || ext.name,
|
||||
version: ext.version,
|
||||
author: ext.namespace,
|
||||
icon: null,
|
||||
enabled: true,
|
||||
icon: ext.icon,
|
||||
enabled: ext.enabled,
|
||||
description: ext.description,
|
||||
homepage: ext.homepage,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Extensions:', error)
|
||||
@ -192,54 +185,6 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* const installAsync = async (extensionDirectory: string | null) => {
|
||||
try {
|
||||
if (!extensionDirectory)
|
||||
throw new Error('Kein Ordner für Erweiterung angegeben')
|
||||
const manifestPath = await join(extensionDirectory, manifestFileName)
|
||||
const manifest = (await JSON.parse(
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
const destination = await getExtensionPathAsync(
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
)
|
||||
|
||||
await checkSourceExtensionDirectoryAsync(extensionDirectory)
|
||||
|
||||
await invoke('copy_directory', {
|
||||
source: extensionDirectory,
|
||||
destination,
|
||||
})
|
||||
|
||||
const logoFilePath = await join(destination, logoFileName)
|
||||
const logo = await readTextFile(logoFilePath)
|
||||
|
||||
const { currentVault } = storeToRefs(useVaultStore())
|
||||
const res = await currentVault.value?.drizzle
|
||||
.insert(haexExtensions)
|
||||
.values({
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
author: manifest.author,
|
||||
enabled: true,
|
||||
url: manifest.url,
|
||||
version: manifest.version,
|
||||
icon: logo,
|
||||
})
|
||||
|
||||
console.log('insert extensions', res)
|
||||
addNotificationAsync({
|
||||
type: 'success',
|
||||
text: `${manifest.name} wurde installiert`,
|
||||
})
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
throw error
|
||||
}
|
||||
} */
|
||||
|
||||
const removeExtensionAsync = async (extensionId: string, version: string) => {
|
||||
try {
|
||||
await invoke('remove_extension', {
|
||||
@ -356,70 +301,6 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
||||
})
|
||||
return preview.value
|
||||
}
|
||||
/* const readManifestFileAsync = async (
|
||||
extensionId: string,
|
||||
version: string,
|
||||
) => {
|
||||
try {
|
||||
if (!(await isExtensionInstalledAsync({ id: extensionId, version })))
|
||||
return null
|
||||
|
||||
const extensionPath = await getExtensionPathAsync(
|
||||
extensionId,
|
||||
`${version}`,
|
||||
)
|
||||
const manifestPath = await join(extensionPath, manifestFileName)
|
||||
const manifest = (await JSON.parse(
|
||||
await readTextFile(manifestPath),
|
||||
)) as IHaexHubExtensionManifest
|
||||
|
||||
return manifest
|
||||
} catch (error) {
|
||||
addNotificationAsync({ type: 'error', text: JSON.stringify(error) })
|
||||
console.error('ERROR readManifestFileAsync', error)
|
||||
}
|
||||
} */
|
||||
|
||||
/* const extensionEntry = computedAsync(
|
||||
async () => {
|
||||
try {
|
||||
|
||||
|
||||
if (!currentExtension.value?.id || !currentExtension.value.version) {
|
||||
console.log('extension id or entry missing', currentExtension.value)
|
||||
return '' // "no mani: " + currentExtension.value;
|
||||
}
|
||||
|
||||
const extensionPath = await getExtensionPathAsync(
|
||||
currentExtension.value?.id,
|
||||
currentExtension.value?.version,
|
||||
) //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
|
||||
|
||||
console.log('extensionEntry extensionPath', extensionPath)
|
||||
const manifest = await readManifestFileAsync(
|
||||
currentExtension.value.id,
|
||||
currentExtension.value.version,
|
||||
)
|
||||
|
||||
if (!manifest) return '' //"no manifest readable";
|
||||
|
||||
//const entryPath = await join(extensionPath, manifest.entry)
|
||||
|
||||
const hexName = stringToHex(
|
||||
JSON.stringify({
|
||||
id: currentExtension.value.id,
|
||||
version: currentExtension.value.version,
|
||||
}),
|
||||
)
|
||||
|
||||
return `haex-extension://${hexName}`
|
||||
} catch (error) {
|
||||
console.error('ERROR extensionEntry', error)
|
||||
}
|
||||
},
|
||||
null,
|
||||
{ lazy: true },
|
||||
) */
|
||||
|
||||
return {
|
||||
availableExtensions,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// stores/extensions/tabs.ts
|
||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||
import { getExtensionWindow } from '~/composables/extensionMessageHandler'
|
||||
|
||||
interface ExtensionTab {
|
||||
extension: IHaexHubExtension
|
||||
@ -40,6 +41,12 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
||||
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)
|
||||
@ -78,8 +85,25 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
||||
// 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 = Date.now()
|
||||
newTab.lastAccessed = now
|
||||
activeTabId.value = extensionId
|
||||
}
|
||||
}
|
||||
@ -113,8 +137,12 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
||||
}
|
||||
|
||||
const broadcastToAllTabs = (message: unknown) => {
|
||||
openTabs.value.forEach(({ iframe }) => {
|
||||
iframe?.contentWindow?.postMessage(message, '*')
|
||||
openTabs.value.forEach(({ extension }) => {
|
||||
// Use sandbox-compatible window reference
|
||||
const win = getExtensionWindow(extension.id)
|
||||
if (win) {
|
||||
win.postMessage(message, '*')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
23
src/types/haexhub.d.ts
vendored
23
src/types/haexhub.d.ts
vendored
@ -20,10 +20,9 @@ export interface IHaexHubExtensionManifest {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IHaexHubExtensionLink extends IHaexHubExtension {
|
||||
installed: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Installed extension from database/backend
|
||||
*/
|
||||
export interface IHaexHubExtension {
|
||||
id: string
|
||||
name: string
|
||||
@ -31,4 +30,20 @@ export interface IHaexHubExtension {
|
||||
author: string | null
|
||||
icon: string | null
|
||||
enabled: boolean
|
||||
description: string | null
|
||||
homepage: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Marketplace extension with additional metadata
|
||||
* Extends IHaexHubExtension with marketplace-specific fields
|
||||
*/
|
||||
export interface IMarketplaceExtension extends Omit<IHaexHubExtension, 'enabled'> {
|
||||
downloads: number
|
||||
rating: number
|
||||
verified: boolean
|
||||
tags: string[]
|
||||
category: string
|
||||
downloadUrl: string
|
||||
isInstalled: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user