mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
extensions fixed
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ dist-ssr
|
|||||||
src-tauri/target
|
src-tauri/target
|
||||||
nogit*
|
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.
|
// 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 display_name: Option<String>,
|
||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
pub allowed_origin: String,
|
pub allowed_origin: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub homepage: Option<String>,
|
||||||
|
pub icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionInfoResponse {
|
impl ExtensionInfoResponse {
|
||||||
@ -204,11 +208,7 @@ impl ExtensionInfoResponse {
|
|||||||
) -> Result<Self, ExtensionError> {
|
) -> Result<Self, ExtensionError> {
|
||||||
use crate::extension::core::types::get_tauri_origin;
|
use crate::extension::core::types::get_tauri_origin;
|
||||||
|
|
||||||
// In development mode, use a wildcard for localhost to match any port
|
// Always use the current Tauri origin to support all platforms (Desktop, Android, iOS)
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
let allowed_origin = "http://localhost:3003".to_string();
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let allowed_origin = get_tauri_origin();
|
let allowed_origin = get_tauri_origin();
|
||||||
|
|
||||||
let key_hash = extension.manifest.calculate_key_hash()?;
|
let key_hash = extension.manifest.calculate_key_hash()?;
|
||||||
@ -222,6 +222,10 @@ impl ExtensionInfoResponse {
|
|||||||
display_name: Some(extension.manifest.name.clone()),
|
display_name: Some(extension.manifest.name.clone()),
|
||||||
namespace: extension.manifest.author.clone(),
|
namespace: extension.manifest.author.clone(),
|
||||||
allowed_origin,
|
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!("Path: {}", path_str);
|
||||||
println!("Asset to load: {}", asset_to_load);
|
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(
|
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||||
app_handle,
|
app_handle,
|
||||||
&state,
|
&state,
|
||||||
|
|||||||
@ -47,7 +47,9 @@ pub fn get_tauri_origin() -> String {
|
|||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[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")]
|
#[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 }}
|
{{ extension.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Stats -->
|
<!-- Stats and Status -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-4 mt-3 text-sm text-gray-500 dark:text-gray-400"
|
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
|
<div
|
||||||
v-if="extension.downloads"
|
v-if="extension.downloads"
|
||||||
class="flex items-center gap-1"
|
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
|
// composables/extensionContextBroadcast.ts
|
||||||
|
// NOTE: This composable is deprecated. Use tabsStore.broadcastToAllTabs() instead.
|
||||||
|
// Keeping for backwards compatibility.
|
||||||
|
|
||||||
|
import { getExtensionWindow } from './extensionMessageHandler'
|
||||||
|
|
||||||
export const useExtensionContextBroadcast = () => {
|
export const useExtensionContextBroadcast = () => {
|
||||||
// Globaler State für alle aktiven IFrames
|
// Globaler State für Extension IDs statt IFrames
|
||||||
const extensionIframes = useState<Set<HTMLIFrameElement>>(
|
const extensionIds = useState<Set<string>>(
|
||||||
'extension-iframes',
|
'extension-ids',
|
||||||
() => new Set(),
|
() => new Set(),
|
||||||
)
|
)
|
||||||
|
|
||||||
const registerExtensionIframe = (iframe: HTMLIFrameElement) => {
|
const registerExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
||||||
extensionIframes.value.add(iframe)
|
extensionIds.value.add(extensionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => {
|
const unregisterExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
||||||
extensionIframes.value.delete(iframe)
|
extensionIds.value.delete(extensionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const broadcastContextChange = (context: {
|
const broadcastContextChange = (context: {
|
||||||
@ -25,8 +30,11 @@ export const useExtensionContextBroadcast = () => {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionIframes.value.forEach((iframe) => {
|
extensionIds.value.forEach((extensionId) => {
|
||||||
iframe.contentWindow?.postMessage(message, '*')
|
const win = getExtensionWindow(extensionId)
|
||||||
|
if (win) {
|
||||||
|
win.postMessage(message, '*')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,8 +48,11 @@ export const useExtensionContextBroadcast = () => {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionIframes.value.forEach((iframe) => {
|
extensionIds.value.forEach((extensionId) => {
|
||||||
iframe.contentWindow?.postMessage(message, '*')
|
const win = getExtensionWindow(extensionId)
|
||||||
|
if (win) {
|
||||||
|
win.postMessage(message, '*')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,10 @@ interface ExtensionRequest {
|
|||||||
// Globaler Handler - nur einmal registriert
|
// Globaler Handler - nur einmal registriert
|
||||||
let globalHandlerRegistered = false
|
let globalHandlerRegistered = false
|
||||||
const iframeRegistry = new Map<HTMLIFrameElement, IHaexHubExtension>()
|
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
|
// Store context values that need to be accessed outside setup
|
||||||
let contextGetters: {
|
let contextGetters: {
|
||||||
@ -22,36 +26,44 @@ let contextGetters: {
|
|||||||
const registerGlobalMessageHandler = () => {
|
const registerGlobalMessageHandler = () => {
|
||||||
if (globalHandlerRegistered) return
|
if (globalHandlerRegistered) return
|
||||||
|
|
||||||
|
console.log('[Parent Debug] ⭐ NEW VERSION LOADED - NO MORE CONTENTWINDOW ⭐')
|
||||||
|
|
||||||
window.addEventListener('message', async (event: MessageEvent) => {
|
window.addEventListener('message', async (event: MessageEvent) => {
|
||||||
// Finde die Extension für dieses IFrame
|
// Ignore console.forward messages - they're handled elsewhere
|
||||||
let extension: IHaexHubExtension | undefined
|
if (event.data?.type === 'console.forward') {
|
||||||
let sourceIframe: HTMLIFrameElement | undefined
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for (const [iframe, ext] of iframeRegistry.entries()) {
|
// Finde die Extension für dieses event.source (sandbox-compatible)
|
||||||
if (event.source === iframe.contentWindow) {
|
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
|
extension = ext
|
||||||
sourceIframe = iframe
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
} else if (extension && !extensionToWindowMap.has(extension.id)) {
|
||||||
|
// Also register in reverse map for broadcasting
|
||||||
|
extensionToWindowMap.set(extension.id, event.source as Window)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!extension || !sourceIframe) {
|
if (!extension) {
|
||||||
return // Message ist nicht von einem registrierten IFrame
|
return // Message ist nicht von einem registrierten IFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = event.data as ExtensionRequest
|
const request = event.data as ExtensionRequest
|
||||||
|
|
||||||
if (!request.id || !request.method) {
|
if (!request.id || !request.method) {
|
||||||
console.error('Invalid extension request:', request)
|
console.error('[ExtensionHandler] Invalid extension request:', request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[HaexHub] ${extension.name} request:`,
|
|
||||||
request.method,
|
|
||||||
request.params,
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result: unknown
|
let result: unknown
|
||||||
|
|
||||||
@ -73,17 +85,25 @@ const registerGlobalMessageHandler = () => {
|
|||||||
throw new Error(`Unknown method: ${request.method}`)
|
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,
|
id: request.id,
|
||||||
result,
|
result,
|
||||||
},
|
},
|
||||||
'*',
|
targetOrigin,
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} 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,
|
id: request.id,
|
||||||
error: {
|
error: {
|
||||||
@ -92,7 +112,7 @@ const registerGlobalMessageHandler = () => {
|
|||||||
details: error,
|
details: error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'*',
|
targetOrigin,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -153,9 +173,26 @@ export const registerExtensionIFrame = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const unregisterExtensionIFrame = (iframe: HTMLIFrameElement) => {
|
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)
|
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
|
// Extension Methods
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -165,10 +202,18 @@ async function handleExtensionMethodAsync(
|
|||||||
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
||||||
) {
|
) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'extension.getInfo':
|
case 'extension.getInfo': {
|
||||||
return await invoke('get_extension_info', {
|
const info = await invoke('get_extension_info', {
|
||||||
extensionId: extension.id,
|
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:
|
default:
|
||||||
throw new Error(`Unknown extension method: ${request.method}`)
|
throw new Error(`Unknown extension method: ${request.method}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #links>
|
<template #links>
|
||||||
<HaexMenuApplications :block="isSmallScreen" />
|
<HaexExtensionLauncher :block="isSmallScreen" />
|
||||||
<UiDropdownVault :block="isSmallScreen" />
|
<UiDropdownVault :block="isSmallScreen" />
|
||||||
</template>
|
</template>
|
||||||
</UPageHeader>
|
</UPageHeader>
|
||||||
|
|||||||
@ -27,14 +27,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UButton>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- IFrame Container -->
|
<!-- IFrame Container -->
|
||||||
<div class="flex-1 relative min-h-0">
|
<div class="flex-1 relative min-h-0">
|
||||||
|
<!-- Extension IFrames -->
|
||||||
<div
|
<div
|
||||||
v-for="tab in tabsStore.sortedTabs"
|
v-for="tab in tabsStore.sortedTabs"
|
||||||
:key="tab.extension.id"
|
:key="tab.extension.id"
|
||||||
:style="{ display: tab.isVisible ? 'block' : 'none' }"
|
:style="{ display: tab.isVisible && !showConsole ? 'block' : 'none' }"
|
||||||
class="absolute inset-0"
|
class="absolute inset-0"
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
@ -48,6 +68,81 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 -->
|
<!-- Loading State -->
|
||||||
<div
|
<div
|
||||||
v-if="tabsStore.tabCount === 0"
|
v-if="tabsStore.tabCount === 0"
|
||||||
@ -68,7 +163,10 @@ import {
|
|||||||
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
|
import { useExtensionTabsStore } from '~/stores/extensions/tabs'
|
||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
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({
|
definePageMeta({
|
||||||
name: 'haexExtension',
|
name: 'haexExtension',
|
||||||
@ -78,6 +176,23 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const tabsStore = useExtensionTabsStore()
|
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
|
// Extension aus Route öffnen
|
||||||
//const extensionId = computed(() => route.params.extensionId as string)
|
//const extensionId = computed(() => route.params.extensionId as string)
|
||||||
|
|
||||||
@ -94,18 +209,70 @@ const dummyIframeRef = ref<HTMLIFrameElement | null>(null)
|
|||||||
const dummyExtensionRef = computed(() => null)
|
const dummyExtensionRef = computed(() => null)
|
||||||
useExtensionMessageHandler(dummyIframeRef, dummyExtensionRef)
|
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) => {
|
const registerIFrame = (extensionId: string, el: HTMLIFrameElement | null) => {
|
||||||
if (!el) return
|
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
|
// Registriere IFrame im Store
|
||||||
tabsStore.registerIFrame(extensionId, el)
|
tabsStore.registerIFrame(extensionId, el)
|
||||||
|
|
||||||
// Registriere IFrame im globalen Message Handler Registry
|
// Registriere IFrame im globalen Message Handler Registry
|
||||||
const tab = tabsStore.openTabs.get(extensionId)
|
const tab = tabsStore.openTabs.get(extensionId)
|
||||||
if (tab?.extension) {
|
if (tab?.extension) {
|
||||||
|
console.log('[Vue Debug] Registering iframe in message handler for:', tab.extension.name)
|
||||||
registerExtensionIFrame(el, tab.extension)
|
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
|
// Cleanup wenn Tabs geschlossen werden
|
||||||
watch(
|
watch(
|
||||||
@ -184,11 +351,16 @@ watch([currentTheme, locale], () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Cleanup beim Verlassen
|
// Copy to clipboard function
|
||||||
onBeforeUnmount(() => {
|
const copyToClipboard = async (text: string) => {
|
||||||
// Optional: Alle Tabs schließen oder offen lassen
|
try {
|
||||||
// tabsStore.closeAllTabs()
|
await navigator.clipboard.writeText(text)
|
||||||
})
|
// Optional: Show success toast
|
||||||
|
console.log('Copied to clipboard')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
|
|||||||
@ -67,27 +67,15 @@
|
|||||||
v-if="filteredExtensions.length"
|
v-if="filteredExtensions.length"
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||||
>
|
>
|
||||||
<template
|
|
||||||
v-for="ext in filteredExtensions"
|
|
||||||
:key="ext.id"
|
|
||||||
>
|
|
||||||
<!-- Installed Extension Card -->
|
|
||||||
<HaexExtensionInstalledCard
|
|
||||||
v-if="ext.isInstalled"
|
|
||||||
:extension="ext"
|
|
||||||
@open="navigateToExtension(ext.id)"
|
|
||||||
@settings="onShowExtensionSettings(ext)"
|
|
||||||
@remove="onShowRemoveDialog(ext)"
|
|
||||||
/>
|
|
||||||
<!-- Marketplace Extension Card -->
|
<!-- Marketplace Extension Card -->
|
||||||
<HaexExtensionMarketplaceCard
|
<HaexExtensionMarketplaceCard
|
||||||
v-else
|
v-for="ext in filteredExtensions"
|
||||||
|
:key="ext.id"
|
||||||
:extension="ext"
|
:extension="ext"
|
||||||
:is-installed="isExtensionInstalled(ext.id)"
|
:is-installed="ext.isInstalled"
|
||||||
@install="onInstallFromMarketplace(ext)"
|
@install="onInstallFromMarketplace(ext)"
|
||||||
@details="onShowExtensionDetails(ext)"
|
@details="onShowExtensionDetails(ext)"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
@ -132,6 +120,7 @@
|
|||||||
import type {
|
import type {
|
||||||
IHaexHubExtension,
|
IHaexHubExtension,
|
||||||
IHaexHubExtensionManifest,
|
IHaexHubExtensionManifest,
|
||||||
|
IMarketplaceExtension,
|
||||||
} from '~/types/haexhub'
|
} from '~/types/haexhub'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
|
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
|
||||||
@ -208,7 +197,7 @@ const categories = computed(() => [
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Dummy Marketplace Extensions (später von API laden)
|
// Dummy Marketplace Extensions (später von API laden)
|
||||||
const marketplaceExtensions = ref([
|
const marketplaceExtensions = ref<IMarketplaceExtension[]>([
|
||||||
{
|
{
|
||||||
id: 'haex-passy',
|
id: 'haex-passy',
|
||||||
name: 'HaexPassDummy',
|
name: 'HaexPassDummy',
|
||||||
@ -217,12 +206,14 @@ const marketplaceExtensions = ref([
|
|||||||
description:
|
description:
|
||||||
'Sicherer Passwort-Manager mit Ende-zu-Ende-Verschlüsselung und Autofill-Funktion.',
|
'Sicherer Passwort-Manager mit Ende-zu-Ende-Verschlüsselung und Autofill-Funktion.',
|
||||||
icon: 'i-heroicons-lock-closed',
|
icon: 'i-heroicons-lock-closed',
|
||||||
|
homepage: null,
|
||||||
downloads: 15420,
|
downloads: 15420,
|
||||||
rating: 4.8,
|
rating: 4.8,
|
||||||
verified: true,
|
verified: true,
|
||||||
tags: ['security', 'password', 'productivity'],
|
tags: ['security', 'password', 'productivity'],
|
||||||
category: 'security',
|
category: 'security',
|
||||||
downloadUrl: '/extensions/haex-pass-1.0.0.haextension',
|
downloadUrl: '/extensions/haex-pass-1.0.0.haextension',
|
||||||
|
isInstalled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'haex-notes',
|
id: 'haex-notes',
|
||||||
@ -232,12 +223,14 @@ const marketplaceExtensions = ref([
|
|||||||
description:
|
description:
|
||||||
'Markdown-basierter Notizen-Editor mit Syntax-Highlighting und Live-Preview.',
|
'Markdown-basierter Notizen-Editor mit Syntax-Highlighting und Live-Preview.',
|
||||||
icon: 'i-heroicons-document-text',
|
icon: 'i-heroicons-document-text',
|
||||||
|
homepage: null,
|
||||||
downloads: 8930,
|
downloads: 8930,
|
||||||
rating: 4.5,
|
rating: 4.5,
|
||||||
verified: true,
|
verified: true,
|
||||||
tags: ['productivity', 'notes', 'markdown'],
|
tags: ['productivity', 'notes', 'markdown'],
|
||||||
category: 'productivity',
|
category: 'productivity',
|
||||||
downloadUrl: '/extensions/haex-notes-2.1.0.haextension',
|
downloadUrl: '/extensions/haex-notes-2.1.0.haextension',
|
||||||
|
isInstalled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'haex-backup',
|
id: 'haex-backup',
|
||||||
@ -247,12 +240,14 @@ const marketplaceExtensions = ref([
|
|||||||
description:
|
description:
|
||||||
'Automatische Backups deiner Daten mit Cloud-Sync-Unterstützung.',
|
'Automatische Backups deiner Daten mit Cloud-Sync-Unterstützung.',
|
||||||
icon: 'i-heroicons-cloud-arrow-up',
|
icon: 'i-heroicons-cloud-arrow-up',
|
||||||
|
homepage: null,
|
||||||
downloads: 5240,
|
downloads: 5240,
|
||||||
rating: 4.6,
|
rating: 4.6,
|
||||||
verified: false,
|
verified: false,
|
||||||
tags: ['backup', 'cloud', 'utilities'],
|
tags: ['backup', 'cloud', 'utilities'],
|
||||||
category: 'utilities',
|
category: 'utilities',
|
||||||
downloadUrl: '/extensions/haex-backup-1.5.2.haextension',
|
downloadUrl: '/extensions/haex-backup-1.5.2.haextension',
|
||||||
|
isInstalled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'haex-calendar',
|
id: 'haex-calendar',
|
||||||
@ -262,12 +257,14 @@ const marketplaceExtensions = ref([
|
|||||||
description:
|
description:
|
||||||
'Integrierter Kalender mit Event-Management und Synchronisation.',
|
'Integrierter Kalender mit Event-Management und Synchronisation.',
|
||||||
icon: 'i-heroicons-calendar',
|
icon: 'i-heroicons-calendar',
|
||||||
|
homepage: null,
|
||||||
downloads: 12100,
|
downloads: 12100,
|
||||||
rating: 4.7,
|
rating: 4.7,
|
||||||
verified: true,
|
verified: true,
|
||||||
tags: ['productivity', 'calendar', 'events'],
|
tags: ['productivity', 'calendar', 'events'],
|
||||||
category: 'productivity',
|
category: 'productivity',
|
||||||
downloadUrl: '/extensions/haex-calendar-3.0.1.haextension',
|
downloadUrl: '/extensions/haex-calendar-3.0.1.haextension',
|
||||||
|
isInstalled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'haex-2fa',
|
id: 'haex-2fa',
|
||||||
@ -277,12 +274,14 @@ const marketplaceExtensions = ref([
|
|||||||
description:
|
description:
|
||||||
'2-Faktor-Authentifizierung Manager mit TOTP und Backup-Codes.',
|
'2-Faktor-Authentifizierung Manager mit TOTP und Backup-Codes.',
|
||||||
icon: 'i-heroicons-shield-check',
|
icon: 'i-heroicons-shield-check',
|
||||||
|
homepage: null,
|
||||||
downloads: 7800,
|
downloads: 7800,
|
||||||
rating: 4.9,
|
rating: 4.9,
|
||||||
verified: true,
|
verified: true,
|
||||||
tags: ['security', '2fa', 'authentication'],
|
tags: ['security', '2fa', 'authentication'],
|
||||||
category: 'security',
|
category: 'security',
|
||||||
downloadUrl: '/extensions/haex-2fa-1.2.0.haextension',
|
downloadUrl: '/extensions/haex-2fa-1.2.0.haextension',
|
||||||
|
isInstalled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'haex-github',
|
id: 'haex-github',
|
||||||
@ -292,39 +291,26 @@ const marketplaceExtensions = ref([
|
|||||||
description:
|
description:
|
||||||
'Direkter Zugriff auf GitHub Repositories, Issues und Pull Requests.',
|
'Direkter Zugriff auf GitHub Repositories, Issues und Pull Requests.',
|
||||||
icon: 'i-heroicons-code-bracket',
|
icon: 'i-heroicons-code-bracket',
|
||||||
|
homepage: null,
|
||||||
downloads: 4120,
|
downloads: 4120,
|
||||||
rating: 4.3,
|
rating: 4.3,
|
||||||
verified: false,
|
verified: false,
|
||||||
tags: ['integration', 'github', 'development'],
|
tags: ['integration', 'github', 'development'],
|
||||||
category: 'integration',
|
category: 'integration',
|
||||||
downloadUrl: '/extensions/haex-github-1.0.5.haextension',
|
downloadUrl: '/extensions/haex-github-1.0.5.haextension',
|
||||||
|
isInstalled: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// Combine installed extensions with marketplace extensions
|
// Mark marketplace extensions as installed if they exist in availableExtensions
|
||||||
const allExtensions = computed(() => {
|
const allExtensions = computed((): IMarketplaceExtension[] => {
|
||||||
// Map installed extensions to marketplace format
|
return marketplaceExtensions.value.map((ext) => ({
|
||||||
const installed = extensionStore.availableExtensions.map((ext) => ({
|
...ext,
|
||||||
id: ext.id,
|
// Check if this marketplace extension is already installed
|
||||||
name: ext.name,
|
isInstalled: extensionStore.availableExtensions.some(
|
||||||
version: ext.version,
|
(installed) => installed.name === ext.name,
|
||||||
author: ext.author || 'Unknown',
|
),
|
||||||
description: 'Installed Extension',
|
|
||||||
icon: ext.icon || 'i-heroicons-puzzle-piece',
|
|
||||||
downloads: 0,
|
|
||||||
rating: 0,
|
|
||||||
verified: false,
|
|
||||||
tags: [],
|
|
||||||
category: 'utilities',
|
|
||||||
downloadUrl: '',
|
|
||||||
isInstalled: true,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
console.log('Installed extensions count:', installed.length)
|
|
||||||
console.log('All extensions:', [...installed, ...marketplaceExtensions.value])
|
|
||||||
|
|
||||||
// Merge with marketplace extensions
|
|
||||||
return [...installed, ...marketplaceExtensions.value]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filtered Extensions
|
// Filtered Extensions
|
||||||
@ -333,7 +319,7 @@ const filteredExtensions = computed(() => {
|
|||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
!searchQuery.value ||
|
!searchQuery.value ||
|
||||||
ext.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
ext.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||||
ext.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
ext.description?.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
|
||||||
const matchesCategory =
|
const matchesCategory =
|
||||||
selectedCategory.value === 'all' ||
|
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
|
// Install from marketplace
|
||||||
const onInstallFromMarketplace = async (ext: unknown) => {
|
const onInstallFromMarketplace = async (ext: unknown) => {
|
||||||
console.log('Install from marketplace:', ext)
|
console.log('Install from marketplace:', ext)
|
||||||
@ -364,35 +342,6 @@ const onShowExtensionDetails = (ext: unknown) => {
|
|||||||
// TODO: Show extension details modal
|
// 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 () => {
|
const onSelectExtensionAsync = async () => {
|
||||||
try {
|
try {
|
||||||
extension.path = await open({ directory: false, recursive: true })
|
extension.path = await open({ directory: false, recursive: true })
|
||||||
@ -405,7 +354,7 @@ const onSelectExtensionAsync = async () => {
|
|||||||
// Check if already installed using full_extension_id
|
// Check if already installed using full_extension_id
|
||||||
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
|
const fullExtensionId = `${preview.value.key_hash}_${preview.value.manifest.name}_${preview.value.manifest.version}`
|
||||||
const isAlreadyInstalled = extensionStore.availableExtensions.some(
|
const isAlreadyInstalled = extensionStore.availableExtensions.some(
|
||||||
ext => ext.id === fullExtensionId
|
(ext) => ext.id === fullExtensionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isAlreadyInstalled) {
|
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'
|
} from '~/types/haexhub'
|
||||||
import type { ExtensionPreview } from '@bindings/ExtensionPreview'
|
import type { ExtensionPreview } from '@bindings/ExtensionPreview'
|
||||||
import type { ExtensionPermissions } from '~~/src-tauri/bindings/ExtensionPermissions'
|
import type { ExtensionPermissions } from '~~/src-tauri/bindings/ExtensionPermissions'
|
||||||
|
import type { ExtensionInfoResponse } from '~~/src-tauri/bindings/ExtensionInfoResponse'
|
||||||
interface ExtensionInfoResponse {
|
|
||||||
keyHash: string
|
|
||||||
name: string
|
|
||||||
fullId: string
|
|
||||||
version: string
|
|
||||||
displayName: string | null
|
|
||||||
namespace: string | null
|
|
||||||
allowedOrigin: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/* const manifestFileName = 'manifest.json'
|
/* const manifestFileName = 'manifest.json'
|
||||||
const logoFileName = 'icon.svg' */
|
const logoFileName = 'icon.svg' */
|
||||||
@ -130,8 +121,10 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
name: ext.displayName || ext.name,
|
name: ext.displayName || ext.name,
|
||||||
version: ext.version,
|
version: ext.version,
|
||||||
author: ext.namespace,
|
author: ext.namespace,
|
||||||
icon: null,
|
icon: ext.icon,
|
||||||
enabled: true,
|
enabled: ext.enabled,
|
||||||
|
description: ext.description,
|
||||||
|
homepage: ext.homepage,
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Extensions:', 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) => {
|
const removeExtensionAsync = async (extensionId: string, version: string) => {
|
||||||
try {
|
try {
|
||||||
await invoke('remove_extension', {
|
await invoke('remove_extension', {
|
||||||
@ -356,70 +301,6 @@ export const useExtensionsStore = defineStore('extensionsStore', () => {
|
|||||||
})
|
})
|
||||||
return preview.value
|
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 {
|
return {
|
||||||
availableExtensions,
|
availableExtensions,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// stores/extensions/tabs.ts
|
// stores/extensions/tabs.ts
|
||||||
import type { IHaexHubExtension } from '~/types/haexhub'
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
||||||
|
import { getExtensionWindow } from '~/composables/extensionMessageHandler'
|
||||||
|
|
||||||
interface ExtensionTab {
|
interface ExtensionTab {
|
||||||
extension: IHaexHubExtension
|
extension: IHaexHubExtension
|
||||||
@ -40,6 +41,12 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
|||||||
return
|
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
|
// Bereits geöffnet? Nur aktivieren
|
||||||
if (openTabs.value.has(extensionId)) {
|
if (openTabs.value.has(extensionId)) {
|
||||||
setActiveTab(extensionId)
|
setActiveTab(extensionId)
|
||||||
@ -78,8 +85,25 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
|||||||
// Zeige neuen Tab
|
// Zeige neuen Tab
|
||||||
const newTab = openTabs.value.get(extensionId)
|
const newTab = openTabs.value.get(extensionId)
|
||||||
if (newTab) {
|
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.isVisible = true
|
||||||
newTab.lastAccessed = Date.now()
|
newTab.lastAccessed = now
|
||||||
activeTabId.value = extensionId
|
activeTabId.value = extensionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,8 +137,12 @@ export const useExtensionTabsStore = defineStore('extensionTabsStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const broadcastToAllTabs = (message: unknown) => {
|
const broadcastToAllTabs = (message: unknown) => {
|
||||||
openTabs.value.forEach(({ iframe }) => {
|
openTabs.value.forEach(({ extension }) => {
|
||||||
iframe?.contentWindow?.postMessage(message, '*')
|
// 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 {
|
export interface IHaexHubExtension {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@ -31,4 +30,20 @@ export interface IHaexHubExtension {
|
|||||||
author: string | null
|
author: string | null
|
||||||
icon: string | null
|
icon: string | null
|
||||||
enabled: boolean
|
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