laden von erweiterungen implementiert

This commit is contained in:
Martin Drechsel
2025-05-15 09:28:45 +02:00
parent a653111071
commit ad3aa4293a
40 changed files with 1454 additions and 784 deletions

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

View File

@ -1,10 +0,0 @@
# .prettierrc.toml
useTabs = false
tabWidth = 2
printWidth = 100
endOfLine = "lf"
# Not supported yet
# trailingComma = "es5"
# embeddedLanguageFormatting = "auto"

7
app.config.ts Normal file
View File

@ -0,0 +1,7 @@
// ~/app.config.ts
export default defineAppConfig({
icon: {
mode: "css",
cssLayer: "base",
},
});

View File

@ -1,3 +1,5 @@
import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
@ -5,22 +7,21 @@ export default defineNuxtConfig({
modules: [
"nuxt-zod-i18n",
"@nuxtjs/i18n",
"@nuxtjs/tailwindcss",
"@pinia/nuxt",
"@vueuse/nuxt",
"@nuxt/icon",
"nuxt-snackbar",
"@nuxt/image",
"nuxt-svgo-loader",
],
imports: {
dirs: ["composables/**", "stores/**", "components/**", "pages/**"],
dirs: ["composables/**", "stores/**", "components/**", "pages/**", "types/**"],
},
i18n: {
strategy: "prefix_and_default",
defaultLocale: "de",
vueI18n: "../src/i18n/i18n.config.ts",
vueI18n: "~/i18n/i18n.config.ts",
locales: [
{ code: "de", language: "de-DE", isCatchallLocale: true },
@ -54,13 +55,18 @@ export default defineNuxtConfig({
},
},
css: ["~/assets/css/main.css"],
devtools: { enabled: true },
srcDir: "./src",
// Enable SSG
ssr: false,
// Enables the development server to be discoverable by other devices when running on iOS physical devices
devServer: { host: process.env.TAURI_DEV_HOST || "localhost", port: 3003 },
vite: {
plugins: [tailwindcss()],
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables
@ -71,11 +77,5 @@ export default defineNuxtConfig({
// Tauri requires a consistent port
strictPort: true,
},
/* plugins: [wasm(), topLevelAwait()],
worker: {
format: 'es',
plugins: () => [wasm(), topLevelAwait()],
}, */
},
});
});

View File

@ -19,6 +19,7 @@
"@nuxt/image": "1.10.0",
"@nuxtjs/i18n": "^9.5.3",
"@pinia/nuxt": "^0.10.1",
"@tailwindcss/vite": "^4.1.5",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
@ -30,21 +31,23 @@
"@vueuse/core": "^13.1.0",
"@vueuse/nuxt": "^13.1.0",
"drizzle-orm": "^0.41.0",
"flyonui": "^2.1.0",
"nuxt": "^3.17.0",
"nuxt-snackbar": "1.3.0",
"nuxt-svgo-loader": "0.5.0",
"nuxt-zod-i18n": "^1.11.5",
"tailwindcss": "^4.1.5",
"vue": "^3.5.13",
"zod": "^3.24.3"
},
"devDependencies": {
"@egoist/tailwindcss-icons": "^1.9.0",
"@iconify/json": "^2.2.332",
"@iconify/tailwind": "^1.2.0",
"@iconify/tailwind4": "^1.0.6",
"@nuxtjs/tailwindcss": "^6.14.0",
"@tauri-apps/cli": "^2.5.0",
"@vitejs/plugin-vue": "^5.2.3",
"drizzle-kit": "^0.30.6",
"flyonui": "^1.3.1",
"typescript": "~5.6.3",
"vite": "^6.3.3",
"vue-tsc": "^2.2.10"

530
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, extension_id, resource, operation, path \n FROM haex_extensions_permissions \n WHERE extension_id = ? AND resource = ? AND operation = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "extension_id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "resource",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "operation",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "path",
"ordinal": 4,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
true,
true,
true,
true
]
},
"hash": "a73e92ff12dca9b046a6440b9a68b002662b594f7f569ee71de11e00c23ca625"
}

5
src-tauri/Cargo.lock generated
View File

@ -1536,6 +1536,7 @@ version = "0.1.0"
dependencies = [
"base64 0.22.1",
"fs_extra",
"hex",
"mime",
"mime_guess",
"rusqlite",
@ -4516,9 +4517,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.1"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes",

View File

@ -24,15 +24,16 @@ rusqlite = { version = "0.35.0", features = [
] }
#libsqlite3-sys = { version = "0.28", features = ["bundled-sqlcipher"] }
#sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.45", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
hex = "0.4"
serde_json = "1"
base64 = "0.22"
mime_guess = "2.0"
mime = "0.3"
fs_extra = "1.3.0"
sqlparser = { version = "0.56.0", features = [] }
tauri = { version = "2.5", features = ["protocol-asset", "custom-protocol"] }
tauri = { version = "2.5", features = ["protocol-asset"] }
tauri-plugin-dialog = "2.2"
tauri-plugin-fs = "2.2.0"
tauri-plugin-opener = "2.2"

View File

@ -227,5 +227,5 @@ pub fn open_encrypted_database(
let mut db = state.0.lock().map_err(|e| e.to_string())?;
*db = Some(conn);
Ok(format!("Verschlüsselte CRDT-Datenbank geöffnet: {}", path))
Ok(format!("success"))
}

View File

@ -1,12 +1,70 @@
use mime;
use serde::Deserialize;
use std::fmt;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use tauri::{
http::{Request, Response, Uri},
AppHandle, Error as TauriError, Manager, Runtime,
http::{Request, Response},
AppHandle, Error as TauriError, Manager, Runtime, UriSchemeContext,
};
#[derive(Deserialize, Debug)]
struct ExtensionInfo {
id: String,
version: String,
}
#[derive(Debug)]
enum DataProcessingError {
HexDecoding(hex::FromHexError),
Utf8Conversion(std::string::FromUtf8Error),
JsonParsing(serde_json::Error),
}
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
impl fmt::Display for DataProcessingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
DataProcessingError::Utf8Conversion(e) => {
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
}
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
}
}
}
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
impl std::error::Error for DataProcessingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DataProcessingError::HexDecoding(e) => Some(e),
DataProcessingError::Utf8Conversion(e) => Some(e),
DataProcessingError::JsonParsing(e) => Some(e),
}
}
}
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
impl From<hex::FromHexError> for DataProcessingError {
fn from(err: hex::FromHexError) -> Self {
DataProcessingError::HexDecoding(err)
}
}
impl From<std::string::FromUtf8Error> for DataProcessingError {
fn from(err: std::string::FromUtf8Error) -> Self {
DataProcessingError::Utf8Conversion(err)
}
}
impl From<serde_json::Error> for DataProcessingError {
fn from(err: serde_json::Error) -> Self {
DataProcessingError::JsonParsing(err)
}
}
pub fn copy_directory(source: String, destination: String) -> Result<(), String> {
println!(
"Kopiere Verzeichnis von '{}' nach '{}'",
@ -46,6 +104,7 @@ pub fn copy_directory(source: String, destination: String) -> Result<(), String>
pub fn resolve_secure_extension_asset_path<R: Runtime>(
app_handle: &AppHandle<R>,
extension_id: &str,
extension_version: &str,
requested_asset_path: &str,
) -> Result<PathBuf, String> {
// 1. Validiere die Extension ID
@ -57,6 +116,17 @@ pub fn resolve_secure_extension_asset_path<R: Runtime>(
return Err(format!("Ungültige Extension ID: {}", extension_id));
}
if extension_version.is_empty()
|| !extension_version
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
{
return Err(format!(
"Ungültige Extension Version: {}",
extension_version
));
}
// 2. Bestimme das Basisverzeichnis für alle Erweiterungen (Resource Directory)
let base_extensions_dir = app_handle
.path()
@ -66,7 +136,8 @@ pub fn resolve_secure_extension_asset_path<R: Runtime>(
.join("extensions");
// 3. Verzeichnis für die spezifische Erweiterung
let specific_extension_dir = base_extensions_dir.join(extension_id);
let specific_extension_dir =
base_extensions_dir.join(format!("{}/{}", extension_id, extension_version));
// 4. Bereinige den angeforderten Asset-Pfad
let clean_relative_path = requested_asset_path
@ -112,70 +183,106 @@ pub fn resolve_secure_extension_asset_path<R: Runtime>(
}
}
pub fn handle_extension_protocol<R: Runtime>(
app_handle: &AppHandle<R>,
pub fn extension_protocol_handler<R: Runtime>(
context: &UriSchemeContext<'_, R>,
request: &Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let uri_ref = request.uri(); // uri_ref ist &Uri
let uri_ref = request.uri();
println!("Protokoll Handler für: {}", uri_ref);
let uri_string = uri_ref.to_string(); // Konvertiere zu String
let parsed_uri = Uri::from_str(&uri_string)?; // Parse aus &str
let extension_id = parsed_uri
let host = uri_ref
.host()
.ok_or("Kein Host (Extension ID) in URI gefunden")?
.to_string();
let requested_asset_path = parsed_uri.path();
let path_str = uri_ref.path();
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
let asset_path_to_load = if requested_asset_path == "/" || requested_asset_path.is_empty() {
"index.html"
} else {
requested_asset_path
};
match process_hex_encoded_json(&host) {
Ok(info) => {
println!("Daten erfolgreich verarbeitet:");
println!(" ID: {}", info.id);
println!(" Version: {}", info.version);
let absolute_secure_path = resolve_secure_extension_asset_path(
context.app_handle(),
&info.id,
&info.version,
&raw_asset_path,
)?;
// Sicheren Dateisystempfad auflösen (nutzt jetzt AppHandle)
let absolute_secure_path =
resolve_secure_extension_asset_path(app_handle, &extension_id, asset_path_to_load)?;
println!("absolute_secure_path: {}", absolute_secure_path.display());
// Datei lesen und Response erstellen (Code wie vorher)
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
println!(
"Liefere {} ({}) für Extension '{}'",
absolute_secure_path.display(),
mime_type,
extension_id
);
// *** KORREKTUR: Verwende Response::builder() ***
Response::builder()
.status(200)
.header("Content-Type", mime_type) // Setze Header über .header()
.body(content) // body() gibt Result<Response<Vec<u8>>, Error> zurück
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
println!(
"Liefere {} ({}) ",
absolute_secure_path.display(),
mime_type
);
Response::builder()
.status(200)
.header("Content-Type", mime_type)
.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)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
}
}
} else {
// Datei nicht gefunden oder es ist keine Datei
eprintln!(
"Asset nicht gefunden oder ist kein File: {}",
absolute_secure_path.display()
);
Response::builder()
.status(404) // HTTP 404 Not Found
.body(Vec::new())
.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
};
// *** KORREKTUR: Verwende Response::builder() auch für Fehler ***
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
Response::builder()
.status(status_code)
.status("500")
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
.map_err(|e| e.into())
}
}
}
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
// Schritt 1: Hex-String zu Bytes dekodieren
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
// Schritt 2: Bytes zu UTF-8-String konvertieren
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
// Schritt 3: JSON-String zu Struktur parsen
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
Ok(extension_info)
}

View File

@ -12,30 +12,37 @@ pub fn run() {
let protocol_name = "haex-extension";
tauri::Builder::default()
/* .register_uri_scheme_protocol(protocol_name, move |app_handle, request| {
// Extrahiere den Request aus dem Kontext
//let request = context.request();
// Rufe die Handler-Logik auf
match extension::core::handle_extension_protocol(0, &request) {
Ok(response) => response, // Gib die erfolgreiche Response zurück
.register_uri_scheme_protocol(protocol_name, move |context, request| {
match extension::core::extension_protocol_handler(&context, &request) {
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
Err(e) => {
// Logge den Fehler
eprintln!("Fehler im Protokoll-Handler für '{}': {}", request.uri(), e);
// Gib eine generische 500er Fehler-Response zurück
Response::builder()
.status(500)
.mimetype("text/plain") // Einfacher Text für die Fehlermeldung
.body(format!("Internal Server Error: {}", e).into_bytes()) // Body als Vec<u8>
.unwrap() // .body() kann hier nicht fehlschlagen
// Wenn der Handler einen Fehler zurückgibt, logge ihn und erstelle eine Fehler-Response
eprintln!(
"Fehler im Custom Protocol Handler für URI '{}': {}",
request.uri(),
e
);
// Erstelle eine HTTP 500 Fehler-Response
// Du kannst hier auch spezifischere Fehler-Responses bauen, falls gewünscht.
tauri::http::Response::builder()
.status(500)
.header("Content-Type", "text/plain") // Optional, aber gut für Klarheit
.body(Vec::from(format!(
"Interner Serverfehler im Protokollhandler: {}",
e
)))
.unwrap_or_else(|build_err| {
// Fallback, falls selbst das Erstellen der Fehler-Response fehlschlägt
eprintln!("Konnte Fehler-Response nicht erstellen: {}", build_err);
tauri::http::Response::builder()
.status(500)
.body(Vec::new())
.expect("Konnte minimale Fallback-Response nicht erstellen")
})
}
}
}) */
/* .setup(move |app| {
// Der .setup Hook ist jetzt nur noch für andere Initialisierungen da
// Der AppHandle ist hier nicht mehr nötig für die Protokoll-Registrierung
println!("App Setup abgeschlossen.");
Ok(())
}) */
//extension::core::extension_protocol_handler(&context, &request)
})
.plugin(tauri_plugin_http::init())
.manage(DbConnection(Mutex::new(None)))
.manage(ExtensionState::default())
@ -57,3 +64,29 @@ pub fn run() {
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
/* fn extension_protocol_handler(
app_handle: &tauri::AppHandle, // Beachten Sie die Signaturänderung in neueren Tauri-Versionen
request: &tauri::http::Request<Vec<u8>>,
) -> Result<tauri::http::Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
let uri_str = request.uri().to_string();
let parsed_url = match Url::parse(&uri_str) {
Ok(url) => url,
Err(e) => {
eprintln!("Fehler beim Parsen der URL '{}': {}", uri_str, e);
return Ok(tauri::http::ResponseBuilder::new().status(400).body(Vec::from("Ungültige URL"))?);
}
};
let plugin_id = parsed_url.host_str().ok_or_else(|| "Fehlende Plugin-ID in der URL".to_string())?;
let path_segments: Vec<&str> = parsed_url.path_segments().ok_or_else(|| "URL hat keinen Pfad".to_string())?.collect();
if path_segments.len() < 2 {
eprintln!("Unvollständiger Pfad in URL: {}", uri_str);
return Ok(tauri::http::Response::new().status(400).body(Vec::from("Unvollständiger Pfad"))?);
}
let version = path_segments;
let file_path = path_segments[1..].join("/");
Ok(tauri::http::Response::builder()::new().status(404).body(Vec::new())?)
} */

View File

@ -7,7 +7,7 @@
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:3003",
"beforeBuildCommand": "pnpm generate",
"frontendDist": "../dist"
"frontendDist": "../.output/public"
},
"app": {
"windows": [
@ -18,7 +18,14 @@
}
],
"security": {
"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost",
"csp": {
"default-src": ["'self'", "haex-extensions"],
"script-src": ["'self'", "haex-extensions"],
"style-src": ["'self'", "haex-extensions"],
"connect-src": ["'self'", "haex-extensions"],
"img-src": ["'self'", "haex-extensions", "data:"],
"font-src": ["'self'", "haex-extensions", "data:"]
},
"assetProtocol": {
"enable": true,
"scope": ["$RESOURCE/extensions/**"]

9
src/assets/css/main.css Normal file
View File

@ -0,0 +1,9 @@
@import "tailwindcss";
@import "flyonui/variants.css";
@plugin "@iconify/tailwind4";
@plugin "flyonui" {
themes: all;
}
@source "../../node_modules/flyonui/flyonui.js";

View File

@ -0,0 +1,53 @@
<template>
<div class="card">
<slot name="image" />
<div class="absolute top-2 right-2">
<UiButton class="btn-error btn-outline btn-sm btn-square" @click="$emit('remove')">
<Icon name="mdi:trash" />
</UiButton>
</div>
<div class="card-header">
<div v-if="$slots.title || name">
<div class="flex justify-start gap-4">
<div v-html="icon" class="shrink-0 size-10" />
<h5 v-if="name" class="card-title m-0 my-auto">
{{ name }}
</h5>
</div>
</div>
<div class="text-base-content/50">{{ manifest }}</div>
</div>
<div class="card-body">
<slot />
<div class="card-actions" v-if="$slots.action">
<slot name="action" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { IHaexHubExtension } from "~/types/haexhub";
const emit = defineEmits(["close", "submit", "remove"]);
defineProps<IHaexHubExtension>();
const { escape, enter } = useMagicKeys();
watchEffect(async () => {
if (escape.value) {
await nextTick();
emit("close");
}
});
watchEffect(async () => {
if (enter.value) {
await nextTick();
emit("submit");
}
});
</script>

View File

@ -0,0 +1,54 @@
<template>
<UiDialog :title="t('title')" v-model:open="open">
<div>
<i18n-t keypath="question" tag="p">
<template #name>
<span class="font-bold text-primary">{{ extension?.name }}</span>
</template>
</i18n-t>
</div>
<template #buttons>
<UiButton class="btn-outline btn-error" @click="open = false">
<Icon name="mdi:cancel" /> {{ t("abort") }}
</UiButton>
<UiButton class="btn-error" @click="onConfirm">
<Icon name="mdi:trash" /> {{ t("remove") }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import type { IHaexHubExtension } from "~/types/haexhub";
const emit = defineEmits(["confirm"]);
const { t } = useI18n();
defineProps<{ extension?: IHaexHubExtension }>();
const open = defineModel<boolean>("open");
const onConfirm = () => {
open.value = false;
emit("confirm");
};
</script>
<i18n lang="json">
{
"de": {
"title": "Erweiterung löschen",
"question": "Soll {name} wirklich gelöscht werden?",
"abort": "Abbrechen",
"remove": "Löschen"
},
"en": {
"title": "Remove Extension",
"question": "Soll {name} wirklich gelöscht werden?",
"abort": "Abort",
"remove": "Remove"
}
}
</i18n>

View File

@ -54,10 +54,12 @@
</template>
<script setup lang="ts">
import type { IHaexHubExtensionManifest } from "~/types/haexhub";
const { t } = useI18n();
const open = defineModel<boolean>("open", { default: false });
defineProps<{ manifest?: IHaexHubExtensionManifest }>();
defineProps<{ manifest?: IHaexHubExtensionManifest | null }>();
const emit = defineEmits(["deny", "confirm"]);

View File

@ -1,25 +1,14 @@
<template>
<button
class="btn join-item"
:class="{
'btn-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
:type
>
<button class="btn join-item" :type>
<slot />
</button>
</template>
<script setup lang="ts">
const { currentScreenSize } = storeToRefs(useUiStore());
defineProps({
type: {
type: String as PropType<'reset' | 'submit' | 'button'>,
default: 'button',
type: String as PropType<"reset" | "submit" | "button">,
default: "button",
},
});
</script>

View File

@ -50,27 +50,10 @@ export interface IDom {
const id = useId();
defineProps({
trigger: {
type: Object as PropType<IDom>,
default: () => ({
class: "",
text: "",
}),
},
title: {
type: String,
default: "",
},
description: {
type: Object as PropType<IDom>,
default: () => ({
class: "",
text: "",
}),
required: false,
},
});
const open = defineModel<boolean>("open", { default: false });
@ -84,8 +67,8 @@ watch(open, async () => {
if (open.value) {
await modal.value?.open();
} else {
const res = await modal.value?.close(true);
console.log("close dialog", res);
await modal.value?.close(true);
console.log("close dialog");
}
});

View File

@ -1,6 +1,6 @@
<template>
<span>
<fieldset class="join w-full">
<!-- <fieldset class="join w-full">
<slot name="prepend" />
<span class="input-group join-item">
@ -66,30 +66,45 @@
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</UiButton>
<!-- <button
v-if="withCopyButton"
class="btn btn-outline btn-accent join-item h-auto"
:class="{
'btn-sm':
currentScreenSize === 'sm' ||
currentScreenSize === '' ||
currentScreenSize === 'xs',
}"
</fieldset> -->
<fieldset class="join w-full p-1">
<slot name="prepend" />
<div class="input join-item">
<Icon :name="prependIcon" class="my-auto shrink-0" />
<div class="input-floating grow">
<input
:id
:name="name ?? id"
:placeholder="placeholder || label"
:type
:autofocus
class="ps-3"
v-bind="$attrs"
v-model="input"
ref="inputRef"
:readonly="read_only"
/>
<label class="input-floating-label" :for="id">{{ label }}</label>
</div>
<Icon :name="appendIcon" class="my-auto shrink-0" />
</div>
<slot name="append" class="h-auto" />
<UiButton
class="btn-outline btn-accent btn-square join-item h-auto"
@click="copy(`${input}`)"
type="button"
>
<Icon :name="copied ? 'mdi:check' : 'mdi:content-copy'" />
</button> -->
</UiButton>
</fieldset>
<span
class="flex flex-col px-2 pt-0.5"
v-show="errors"
>
<span
v-for="error in errors"
class="label-text-alt text-error"
>
<span class="flex flex-col px-2 pt-0.5" v-show="errors">
<span v-for="error in errors" class="label-text-alt text-error">
{{ error }}
</span>
</span>
@ -97,9 +112,9 @@
</template>
<script setup lang="ts">
import { type ZodSchema } from 'zod';
import { type ZodSchema } from "zod";
const inputRef = useTemplateRef('inputRef');
const inputRef = useTemplateRef("inputRef");
defineExpose({ inputRef });
defineOptions({
@ -109,45 +124,45 @@ defineOptions({
const props = defineProps({
placeholder: {
type: String,
default: '',
default: "",
},
type: {
type: String as PropType<
| 'button'
| 'checkbox'
| 'color'
| 'date'
| 'datetime-local'
| 'email'
| 'file'
| 'hidden'
| 'image'
| 'month'
| 'number'
| 'password'
| 'radio'
| 'range'
| 'reset'
| 'search'
| 'submit'
| 'tel'
| 'text'
| 'time'
| 'url'
| 'week'
| "button"
| "checkbox"
| "color"
| "date"
| "datetime-local"
| "email"
| "file"
| "hidden"
| "image"
| "month"
| "number"
| "password"
| "radio"
| "range"
| "reset"
| "search"
| "submit"
| "tel"
| "text"
| "time"
| "url"
| "week"
>,
default: 'text',
default: "text",
},
label: String,
name: String,
prependIcon: {
type: String,
default: '',
default: "",
},
prependLabel: String,
appendIcon: {
type: String,
default: '',
default: "",
},
appendLabel: String,
rules: Object as PropType<ZodSchema>,
@ -158,7 +173,7 @@ const props = defineProps({
});
const input = defineModel<string | number | undefined | null>({
default: '',
default: "",
required: true,
});
@ -167,7 +182,7 @@ onMounted(() => {
if (props.autofocus && inputRef.value) inputRef.value.focus();
});
const errors = defineModel<string[] | undefined>('errors');
const errors = defineModel<string[] | undefined>("errors");
const id = useId();
@ -180,7 +195,7 @@ watch(
}
);
const emit = defineEmits(['error']);
const emit = defineEmits(["error"]);
const checkInput = () => {
if (props.rules) {
@ -188,7 +203,7 @@ const checkInput = () => {
//console.log('check result', result.error, props.rules);
if (!result.success) {
errors.value = result.error.errors.map((error) => error.message);
emit('error', errors.value);
emit("error", errors.value);
} else {
errors.value = [];
}

View File

@ -9,10 +9,7 @@
v-model="value"
>
<template #append>
<UiButton
class="btn-outline btn-accent h-auto"
@click="tooglePasswordType"
>
<UiButton class="btn-outline btn-accent btn-square h-auto" @click="tooglePasswordType">
<Icon :name="type === 'password' ? 'mdi:eye' : 'mdi:eye-off'" />
</UiButton>
</template>
@ -20,7 +17,7 @@
</template>
<script setup lang="ts">
import type { ZodSchema } from 'zod';
import type { ZodSchema } from "zod";
const { t } = useI18n();
const { currentScreenSize } = storeToRefs(useUiStore());
@ -35,10 +32,10 @@ defineProps({
autofocus: Boolean,
});
const type = ref<'password' | 'text'>('password');
const type = ref<"password" | "text">("password");
const tooglePasswordType = () => {
type.value = type.value === 'password' ? 'text' : 'password';
type.value = type.value === "password" ? "text" : "password";
};
</script>

View File

@ -1,49 +0,0 @@
<template>
<li
@click="triggerNavigate"
class="hover:text-primary rounded"
:class="{ ['bg-base-300']: isActive }"
>
<UiTooltip :tooltip="tooltip ?? name" direction="right-start">
<NuxtLinkLocale
:to="{ name: 'haexExtension', params: { extensionId: props.id } }"
class="flex items-center justify-center cursor-pointer tooltip-toogle"
ref="link"
>
<div v-html="icon" class="shrink-0 size-6" />
<!-- <Icon mode="svg" :name="icon" class="shrink-0 size-6" /> -->
</NuxtLinkLocale>
</UiTooltip>
</li>
</template>
<script setup lang="ts">
import { type ISidebarItem } from "#imports";
const props = defineProps<ISidebarItem>();
console.log("image", props.icon);
const router = useRouter();
const isActive = computed(() => {
if (props.to?.name === "haexExtension") {
return getSingleRouteParam(router.currentRoute.value.params.extensionId) === props.id;
} else {
return props.to?.name === router.currentRoute.value.meta.name;
}
});
const link = useTemplateRef("link");
const triggerNavigate = () => link.value?.$el.click();
/* computed(() => {
const found = useRouter()
.getRoutes()
.find((route) => route.name === useLocaleRoute()(props.to)?.name);
console.log('found route', found, useRoute());
return (
found?.name === useRoute().name ||
found?.children.some((child) => child.name === useRoute().name)
);
}); */
</script>

View File

@ -8,9 +8,10 @@
<NuxtLinkLocale
:to
class="flex items-center justify-center cursor-pointer tooltip-toogle"
ref="link"
ref="linkRef"
>
<Icon :name="icon" class="shrink-0 size-6" />
<div v-if="iconType === 'svg'" v-html="icon" class="shrink-0 size-5" />
<Icon v-else :name="icon" size="1.5em" />
</NuxtLinkLocale>
</UiTooltip>
</li>
@ -23,6 +24,7 @@ const props = defineProps<ISidebarItem>();
const router = useRouter();
console.log("to", props.to);
const isActive = computed(() => {
if (props.to?.name === "haexExtension") {
return getSingleRouteParam(router.currentRoute.value.params.extensionId) === props.id;
@ -31,9 +33,9 @@ const isActive = computed(() => {
}
});
const link = useTemplateRef("link");
const linkRef = useTemplateRef("linkRef");
const triggerNavigate = () => link.value?.$el.click();
const triggerNavigate = () => linkRef.value?.$el.click();
/* computed(() => {
const found = useRouter()

View File

@ -1,52 +1,51 @@
<template>
<UiDialog :title="t('title')" v-model:open="open">
<template #trigger="{ id }">
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap"
@click="open = true"
>
<Icon name="mdi:plus" />
{{ t("database.create") }}
</button>
</template>
<UiDialog :title="t('title')" v-model:open="open">
<template #trigger="{ id }">
<button
class="btn btn-primary btn-outline shadow-md md:btn-lg shrink-0 flex-1 whitespace-nowrap flex-nowrap"
@click="open = true"
>
<Icon name="mdi:plus" />
{{ t("database.create") }}
</button>
</template>
<form class="flex flex-col gap-4" @submit="onCreateAsync">
<!-- @keyup.enter="onCreateAsync" -->
<UiInput
:check-input="check"
:label="t('database.label')"
:placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name"
autofocus
prepend-icon="mdi:safe"
v-model="database.name"
/>
<form class="flex flex-col gap-4" @submit="onCreateAsync">
<!-- @keyup.enter="onCreateAsync" -->
<UiInput
:check-input="check"
:label="t('database.label')"
:placeholder="t('database.placeholder')"
:rules="vaultDatabaseSchema.name"
autofocus
prepend-icon="mdi:safe"
v-model="database.name"
/>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
</form>
<UiInputPassword
:check-input="check"
:rules="vaultDatabaseSchema.password"
prepend-icon="mdi:key-outline"
v-model="database.password"
/>
</form>
<template #buttons>
<UiButton class="btn-error" @click="onClose">
{{ t("abort") }}
</UiButton>
<template #buttons>
<UiButton class="btn-error" @click="onClose">
{{ t("abort") }}
</UiButton>
<UiButton class="btn-primary" @click="onCreateAsync">
{{ t("create") }}
</UiButton>
</template>
</UiDialog>
<UiButton class="btn-primary" @click="onCreateAsync">
{{ t("create") }}
</UiButton>
</template>
</UiDialog>
</template>
<script setup lang="ts">
import { save } from "@tauri-apps/plugin-dialog";
import { useVaultStore } from "~/stores/vault";
import { vaultDatabaseSchema } from "./schema";
import Database from "@tauri-apps/plugin-sql";
const check = ref(false);
const open = ref();
@ -54,22 +53,22 @@ const open = ref();
const { t } = useI18n();
const database = reactive<{
name: string;
password: string;
path: string | null;
type: "password" | "text";
name: string;
password: string;
path: string | null;
type: "password" | "text";
}>({
name: "",
password: "",
path: "",
type: "password",
name: "",
password: "",
path: "",
type: "password",
});
const initDatabase = () => {
database.name = t("database.name");
database.password = "";
database.path = "";
database.type = "password";
database.name = t("database.name");
database.password = "";
database.path = "";
database.type = "password";
};
initDatabase();
@ -78,69 +77,71 @@ const { add } = useSnackbar();
const { createAsync } = useVaultStore();
const onCreateAsync = async () => {
check.value = true;
check.value = true;
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name);
const passwordCheck = vaultDatabaseSchema.password.safeParse(database.password);
const nameCheck = vaultDatabaseSchema.name.safeParse(database.name);
const passwordCheck = vaultDatabaseSchema.password.safeParse(database.password);
console.log("checks", database.name, nameCheck, database.password, passwordCheck);
if (!nameCheck.success || !passwordCheck.success) return;
console.log("checks", database.name, nameCheck, database.password, passwordCheck);
if (!nameCheck.success || !passwordCheck.success) return;
open.value = false;
try {
database.path = await save({
defaultPath: database.name.endsWith(".db") ? database.name : `${database.name}.db`,
});
open.value = false;
try {
database.path = await save({
defaultPath: database.name.endsWith(".db") ? database.name : `${database.name}.db`,
});
console.log("data", database);
console.log("data", database);
if (database.path && database.password) {
const vaultId = await createAsync({
path: database.path,
password: database.password,
});
if (database.path && database.password) {
const vaultId = await createAsync({
path: database.path,
password: database.password,
});
console.log("vaultId", vaultId);
await navigateTo(useLocaleRoute()({ name: "vaultOverview", params: { vaultId } }));
}
} catch (error) {
console.error(error);
add({ type: "error", text: JSON.stringify(error) });
console.log("vaultId", vaultId);
if (vaultId) {
await navigateTo(useLocaleRoute()({ name: "vaultOverview", params: { vaultId } }));
}
}
} catch (error) {
console.error(error);
add({ type: "error", text: JSON.stringify(error) });
}
};
const onClose = () => {
open.value = false;
initDatabase();
open.value = false;
initDatabase();
};
</script>
<i18n lang="json">
{
"de": {
"database": {
"label": "Datenbankname",
"placeholder": "Passwörter",
"create": "Neue Vault anlegen",
"name": "Passwörter"
},
"title": "Neue Datenbank anlegen",
"create": "Erstellen",
"abort": "Abbrechen",
"description": "Haex Vault für deine geheimsten Geheimnisse"
"de": {
"database": {
"label": "Datenbankname",
"placeholder": "Passwörter",
"create": "Neue Vault anlegen",
"name": "Passwörter"
},
"title": "Neue Datenbank anlegen",
"create": "Erstellen",
"abort": "Abbrechen",
"description": "Haex Vault für deine geheimsten Geheimnisse"
},
"en": {
"database": {
"label": "Databasename",
"placeholder": "Databasename",
"create": "Create new Vault",
"name": "Passwords"
},
"title": "Create New Database",
"create": "Create",
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
"en": {
"database": {
"label": "Databasename",
"placeholder": "Databasename",
"create": "Create new Vault",
"name": "Passwords"
},
"title": "Create New Database",
"create": "Create",
"abort": "Abort",
"description": "Haex Vault for your most secret secrets"
}
}
</i18n>

View File

@ -102,6 +102,7 @@ const onLoadDatabase = async () => {
const localePath = useLocalePath();
const { currentVault, currentVaultId } = storeToRefs(useVaultStore());
const onOpenDatabase = async () => {
try {
check.value = true;
@ -110,7 +111,10 @@ const onOpenDatabase = async () => {
const passwordCheck = vaultDatabaseSchema.password.safeParse(database.password);
if (!pathCheck.success || !passwordCheck.success || !path) {
add({ type: "error", text: "params falsch" });
add({
type: "error",
text: `Params falsch. Path: ${pathCheck.error} | Password: ${passwordCheck.error}`,
});
return;
}
@ -131,15 +135,14 @@ const onOpenDatabase = async () => {
onClose();
currentVaultId.value = vaultId;
console.log("vault before navigation", currentVault.value, currentVaultId.value, vaultId);
await navigateTo(
localePath({
name: "vaultOverview",
params: {
vaultId,
},
query: {
showSidebar: "true",
},
})
);
} catch (error) {

View File

@ -3,14 +3,19 @@
<slot name="image" />
<div class="card-header">
<h5 class="card-title" v-if="$slots.title">
<slot name="title" />
</h5>
<div v-if="$slots.title || title">
<Icon :name="icon" />
<h5 v-if="title" class="card-title mb-0">
{{ title }}
</h5>
<slot v-else name="title" />
</div>
<div class="text-base-content/50">Your journey starts here</div>
</div>
<div class="card-body">
<slot />
aaaaaaaaa
<div class="card-actions" v-if="$slots.action">
<slot name="action" />
</div>
@ -36,6 +41,8 @@
<script setup lang="ts">
const emit = defineEmits(["close", "submit"]);
defineProps<{ title?: string; icon?: string }>();
const { escape, enter } = useMagicKeys();
watchEffect(async () => {

View File

@ -1,5 +1,3 @@
import { H3Error } from "h3";
export const bytesToBase64DataUrlAsync = async (
bytes: Uint8Array,
type = "application/octet-stream"
@ -82,9 +80,36 @@ export const isRouteActive = (to: RouteLocationRawI18n, exact: boolean = false)
return exact
? found?.name === useRouter().currentRoute.value.name
: found?.name === useRouter().currentRoute.value.name ||
found?.children.some((child) => child.name === useRouter().currentRoute.value.name);
found?.children.some((child) => child.name === useRouter().currentRoute.value.name);
});
export const isKey = <T extends object>(x: T, k: PropertyKey): k is keyof T => {
return k in x;
};
export const filterAsync = async <T>(
arr: T[],
predicate: (value: T, index: number, array: T[]) => Promise<boolean>
) => {
// 1. Mappe jedes Element auf ein Promise, das zu true/false auflöst
const results = await Promise.all(arr.map(predicate));
// 2. Filtere das ursprüngliche Array basierend auf den Ergebnissen
return arr.filter((_value, index) => results[index]);
};
export const stringToHex = (str: string) =>
str
.split("")
.map((char) => char.charCodeAt(0).toString(16).padStart(2, "0"))
.join(""); // Join array into a single string
export const hexToString = (hex: string) => {
if (!hex) return "";
const parsedValue = hex
.match(/.{1,2}/g) // Split hex into pairs
?.map((byte) => String.fromCharCode(parseInt(byte, 16))) // Convert hex to char
.join(""); // Join array into a single string
return parsedValue ? parsedValue : "";
};

View File

@ -3,17 +3,22 @@
<nav
class="navbar bg-base-100 rounded-b max-sm:shadow border-b border-base-content/25 sm:z-20 relative px-2"
>
<button
type="button"
class="btn btn-text btn-square me-2 z-50"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
@click="toogleSidebar"
ref="sidebarToogleRef"
>
<Icon name="mage:dash-menu" size="28" />
</button>
<UiTooltip :tooltip="isVisible ? t('sidebar.close') : t('sidebar.show')">
<button
type="button"
class="btn btn-text btn-square me-2 z-50"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="sidebar"
@click="toogleSidebar"
ref="sidebarToogleRef"
>
<Icon
:name="isVisible ? 'tabler:layout-sidebar' : 'tabler:layout-sidebar-filled'"
size="28"
/>
</button>
</UiTooltip>
<div class="flex flex-1 items-center">
<NuxtLinkLocale
@ -156,35 +161,28 @@
<div class="flex h-full">
<aside
id="sidebar"
class="sm:shadow-none drawer max-w-14 transition-all"
:class="[!isVisible ? 'w-0' : 'w-14']"
class="sm:shadow-none transition-all h-full overflow-hidden border-r border-base-300"
:class="[!isVisible ? 'w-0' : 'w-16']"
role="dialog"
tabindex="-1"
>
<div class="drawer-body px-0">
<ul class="menu p-0">
<div class="drawer-body h-full">
<ul class="menu p-0 h-full rounded-none">
<UiSidebarLink v-bind="item" v-for="item in menu" :key="item.id" />
<UiSidebarLinkExtension
v-bind="item"
v-for="item in availableExtensions"
<UiSidebarLink
v-for="item in extensionLinks"
:key="item.id"
v-bind="item"
icon-type="svg"
/>
</ul>
</div>
</aside>
<div class="overflow-hidden transition-all relative w-full">
<div
class="h-full overflow-scroll transition-all pl-0"
:class="[isVisible ? 'sm:pl-14 ' : ' pl-0']"
>
<slot />
</div>
</div>
<main class="w-full">
<NuxtPage :transition="{ name: 'fade' }" />
</main>
</div>
<!-- <main class="sm:pl-14">
<NuxtPage />
</main> -->
</div>
</template>
@ -194,18 +192,19 @@ import { NuxtLinkLocale } from "#components";
const { t } = useI18n();
const { menu, isVisible } = storeToRefs(useSidebarStore());
const sidebarToogleRef = useTemplateRef("sidebarToogleRef");
onClickOutside(sidebarToogleRef, () => {
/* onClickOutside(sidebarToogleRef, () => {
if (currentScreenSize.value === "xs") {
isVisible.value = false;
}
});
}); */
const { notifications } = storeToRefs(useNotificationStore());
const { isActive } = useExtensionsStore();
const { closeAsync } = useVaultStore();
const { currentScreenSize } = storeToRefs(useUiStore());
const onExtensionSelectAsync = async (id: string) => {};
const { availableExtensions } = storeToRefs(useExtensionsStore());
const { extensionLinks } = storeToRefs(useExtensionsStore());
const toogleSidebar = () => {
isVisible.value = !isVisible.value;
};
@ -223,10 +222,16 @@ de:
view_all: Alle ansehen
vault:
close: Vault schließen
sidebar:
close: Sidebar schließen
show: Sidebar anzeigen
en:
notifications:
label: Notifications
view_all: View all
vault:
close: Close vault
sidebar:
close: close sidebar
show: show sidebar
</i18n>

View File

@ -0,0 +1,9 @@
export default defineNuxtRouteMiddleware(async (to) => {
const { openVaults } = storeToRefs(useVaultStore());
const toVaultId = getSingleRouteParam(to.params.vaultId);
if (!openVaults.value?.[toVaultId]) {
return await navigateTo(useLocalePath()({ name: "vaultOpen" }));
}
});

View File

@ -1,6 +1,6 @@
<template>
<div class="items-center justify-center min-h-full flex w-full">
<div class="flex flex-col justify-center items-center gap-4 max-w-3xl">
<div class="flex flex-col justify-center items-center gap-5 max-w-3xl">
<img src="/logo.svg" class="bg-primary p-3 size-16 rounded-full" alt="HaexVault Logo" />
<span class="flex flex-wrap font-bold text-pretty text-xl gap-2 justify-center">
@ -10,7 +10,7 @@
<UiTextGradient>Haex Hub</UiTextGradient>
</span>
<div class="flex flex-col md:flex-row gap-4 w-full">
<div class="flex flex-col md:flex-row gap-4 w-full h-24 md:h-auto">
<VaultButtonCreate />
<VaultButtonOpen v-model:isOpen="passwordPromptOpen" :path="vaultPath" />
@ -52,7 +52,7 @@
:key="vault.path"
>
<button
class="link link-accent flex items-center no-underline justify-between text-nowrap text-xs md:text-base shrink w-full py-2 px-4"
class="link link-accent flex items-center no-underline justify-between text-nowrap text-sm md:text-base shrink w-full py-2 px-4"
@click="
passwordPromptOpen = true;
vaultPath = vault.path;

View File

@ -6,4 +6,8 @@
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
definePageMeta({
middleware: "database",
});
</script>

View File

@ -1,16 +1,16 @@
<template>
<div>
browser {{ useRouter().currentRoute.value.meta.name }}
<HaexBrowser
:tabs="tabs"
:activeTabId="activeTabId"
@createTab="createNewTab"
@closeTab="closeTab"
@navigate="navigateToUrl"
@goBack="goBack"
@goForward="goForward"
/>
</div>
<div class="bg-red-400 h-full">
browser {{ useRouter().currentRoute.value.meta.name }}
<HaexBrowser
:tabs="tabs"
:activeTabId="activeTabId"
@createTab="createNewTab"
@closeTab="closeTab"
@navigate="navigateToUrl"
@goBack="goBack"
@goForward="goForward"
/>
</div>
</template>
<script setup lang="ts">
@ -20,16 +20,16 @@ import { Window, getCurrentWindow } from "@tauri-apps/api/window";
import { Webview } from "@tauri-apps/api/webview";
definePageMeta({
name: "haexBrowser",
name: "haexBrowser",
});
interface Tab {
id: string;
title: string;
url: string;
isLoading: boolean;
isActive: boolean;
window_label: string;
id: string;
title: string;
url: string;
isLoading: boolean;
isActive: boolean;
window_label: string;
}
const tabs = ref<Tab[]>([]);
@ -39,43 +39,43 @@ let unlistenTabCreated: UnlistenFn | null = null;
let unlistenTabClosed: UnlistenFn | null = null;
onMounted(async () => {
// Erstelle einen ersten Tab beim Start
//createNewTab("https://www.google.com");
// Erstelle einen ersten Tab beim Start
//createNewTab("https://www.google.com");
// Höre auf Tab-Events
unlistenTabCreated = await listen("tab-created", (event) => {
const newTab = event.payload as Tab;
// Höre auf Tab-Events
unlistenTabCreated = await listen("tab-created", (event) => {
const newTab = event.payload as Tab;
tabs.value = tabs.value.map((tab) => ({
...tab,
isActive: tab.id === newTab.id,
}));
tabs.value = tabs.value.map((tab) => ({
...tab,
isActive: tab.id === newTab.id,
}));
if (!tabs.value.some((tab) => tab.id === newTab.id)) {
tabs.value.push(newTab);
}
if (!tabs.value.some((tab) => tab.id === newTab.id)) {
tabs.value.push(newTab);
}
activeTabId.value = newTab.id;
});
activeTabId.value = newTab.id;
});
unlistenTabClosed = await listen("tab-closed", (event) => {
const closedTabId = event.payload as string;
tabs.value = tabs.value.filter((tab) => tab.id !== closedTabId);
});
unlistenTabClosed = await listen("tab-closed", (event) => {
const closedTabId = event.payload as string;
tabs.value = tabs.value.filter((tab) => tab.id !== closedTabId);
});
});
onUnmounted(() => {
if (unlistenTabCreated) unlistenTabCreated();
if (unlistenTabClosed) unlistenTabClosed();
if (unlistenTabCreated) unlistenTabCreated();
if (unlistenTabClosed) unlistenTabClosed();
});
const createNewTab = async (url: string = "about:blank") => {
try {
/* const appWindow = new Window('uniqueLabel111', {
try {
/* const appWindow = new Window('uniqueLabel111', {
fullscreen: true,
});
*/
/* const appWindow = getCurrentWindow();
/* const appWindow = getCurrentWindow();
const webview = new Webview(appWindow, 'theUniqueLabel', {
url: 'https://github.com/tauri-apps/tauri',
@ -85,43 +85,43 @@ const createNewTab = async (url: string = "about:blank") => {
y: 0,
});
await webview.show(); */
//console.log('create webview', webview);
const tab_id = "foo";
await invoke("create_tab", { url, tabId: "foo" });
} catch (error) {
console.error("Fehler beim Erstellen des Tabs:", error);
}
//console.log('create webview', webview);
const tab_id = "foo";
await invoke("create_tab", { url, tabId: "foo" });
} catch (error) {
console.error("Fehler beim Erstellen des Tabs:", error);
}
};
const closeTab = async (tabId: string) => {
try {
//await invoke('close_tab', { tabId });
} catch (error) {
console.error("Fehler beim Schließen des Tabs:", error);
}
try {
//await invoke('close_tab', { tabId });
} catch (error) {
console.error("Fehler beim Schließen des Tabs:", error);
}
};
const navigateToUrl = async (tabId: string, url: string) => {
try {
//await invoke('navigate_to_url', { tabId, url });
} catch (error) {
console.error("Fehler bei der Navigation:", error);
}
try {
//await invoke('navigate_to_url', { tabId, url });
} catch (error) {
console.error("Fehler bei der Navigation:", error);
}
};
const goBack = async (tabId: string | null) => {
try {
//await invoke('go_back', { tabId });
} catch (error) {
console.error("Fehler beim Zurückgehen:", error);
}
try {
//await invoke('go_back', { tabId });
} catch (error) {
console.error("Fehler beim Zurückgehen:", error);
}
};
const goForward = async (tabId: string | null) => {
try {
//await invoke('go_forward', { tabId });
} catch (error) {
console.error("Fehler beim Vorwärtsgehen:", error);
}
try {
//await invoke('go_forward', { tabId });
} catch (error) {
console.error("Fehler beim Vorwärtsgehen:", error);
}
};
</script>

View File

@ -1,6 +1,8 @@
<template>
{{ iframeSrc }}
<div class="w-full h-full">
<div class="w-full h-full overflow-scroll bg-red-300">
<div>
{{ iframeSrc }}
</div>
<iframe
v-if="iframeSrc"
class="w-full h-full"
@ -10,7 +12,7 @@
sandbox="allow-scripts allow-same-origin"
>
</iframe>
<p v-else>{{ t("loading") }}</p>
<!-- <p v-else>{{ t("loading") }}</p> -->
</div>
</template>
@ -28,11 +30,11 @@ const extensionStore = useExtensionsStore();
watch(iframeSrc, () => console.log("iframeSrc", iframeSrc.value), { immediate: true });
onMounted(async () => {
const minfest = await extensionStore.readManifestFileAsync(
/* const minfest = await extensionStore.readManifestFileAsync(
currentExtension.value!.id,
currentExtension.value!.version
);
console.log("manifest", minfest, extensionStore.extensionEntry);
console.log("manifest", minfest, extensionStore.extensionEntry); */
});
</script>

View File

@ -1,15 +1,38 @@
<template>
<div class="flex flex-col">
<h1>{{ t("title") }}</h1>
<UiButton @click="loadExtensionManifestAsync">
{{ t("extension.add") }}
<div class="flex flex-col p-1 relative">
<UiButton
class="fixed top-20 right-4 btn-square btn-primary"
@click="loadExtensionManifestAsync"
>
<Icon name="mdi:plus" size="1.5em" />
</UiButton>
<h1>{{ t("title") }}</h1>
<div class="flex">
<HaexExtensionCard
v-for="extension in extensionStore.availableExtensions"
v-bind="extension"
@remove="onShowRemoveDialog(extension)"
>
</HaexExtensionCard>
</div>
<!-- <UiButton @click="loadExtensionManifestAsync">
{{ t("extension.add") }}
</UiButton> -->
<HaexExtensionManifestConfirm
:manifest="extension.manifest!"
:manifest="extension.manifest"
v-model:open="showConfirmation"
@confirm="addExtensionAsync"
/>
{{ showRemoveDialog }}
<HaexExtensionDialogRemove
v-model:open="showRemoveDialog"
:extension="extensionToBeRemoved"
@confirm="removeExtensionAsync"
>
</HaexExtensionDialogRemove>
</div>
</template>
@ -17,6 +40,7 @@
import { join } from "@tauri-apps/api/path";
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";
import type { IHaexHubExtension, IHaexHubExtensionManifest } from "~/types/haexhub";
definePageMeta({
name: "extensionOverview",
@ -35,6 +59,8 @@ const extension = reactive<{
path: "",
});
onMounted(() => console.log("extension overview"));
const loadExtensionManifestAsync = async () => {
try {
extension.path = await open({ directory: true, recursive: true });
@ -50,7 +76,8 @@ const loadExtensionManifestAsync = async () => {
extension.manifest = manifestFile;
showConfirmation.value = true;
} catch (error) {
console.error("Fehler beim Laden des Moduls:", error);
console.error("Fehler loadExtensionManifestAsync:", error);
add({ type: "error", text: JSON.stringify(error) });
}
};
@ -67,26 +94,64 @@ const addExtensionAsync = async () => {
text: t("extension.success.text"),
});
} catch (error) {
console.error("Fehler beim Laden des Moduls:", error);
console.error("Fehler addExtensionAsync:", error);
add({ type: "error", text: JSON.stringify(error) });
}
};
const showRemoveDialog = ref(false);
const extensionToBeRemoved = ref<IHaexHubExtension>();
const onShowRemoveDialog = (extension: IHaexHubExtension) => {
extensionToBeRemoved.value = extension;
showRemoveDialog.value = true;
};
const removeExtensionAsync = async () => {
if (!extensionToBeRemoved.value?.id || !extensionToBeRemoved.value?.version) {
add({ type: "error", text: "Erweiterung kann nicht gelöscht werden" });
return;
}
try {
await extensionStore.removeExtensionAsync(
extensionToBeRemoved.value.id,
extensionToBeRemoved.value.version
);
await extensionStore.loadExtensionsAsync();
add({
type: "success",
title: t("extension.remove.success.title", {
extensionName: extensionToBeRemoved.value.name,
}),
text: t("extension.remove.success.text", { extensionName: extensionToBeRemoved.value.name }),
});
} catch (error) {
add({
type: "error",
title: t("extension.remove.error.title"),
text: t("extension.remove.error.text", { error: JSON.stringify(error) }),
});
}
};
</script>
<i18n lang="json">
{
"de": {
"title": "Erweiterung installieren",
"extension": {
"add": "Erweiterung hinzufügen",
"success": {
"title": "{extension} hinzugefügt",
"text": "Die Erweiterung wurde erfolgreich hinzugefügt"
}
}
},
"en": {
"title": "Install extension"
}
}
<i18n lang="yaml">
de:
title: "Erweiterung installieren"
extension:
remove:
success:
text: "Erweiterung {extensionName} wurde erfolgreich entfernt"
title: "{extensionName} entfernt"
error:
text: "Erweiterung {extensionName} konnte nicht entfernt werden. \n {error}"
title: "Fehler beim Entfernen von {extensionName}"
add: "Erweiterung hinzufügen"
success:
title: "{extension} hinzugefügt"
text: "Die Erweiterung wurde erfolgreich hinzugefügt"
en:
title: "Install extension"
</i18n>

View File

@ -1,5 +1,5 @@
<template>
<div class="bg-blue-200 h-full">aaaaa</div>
<div class="h-full"></div>
</template>
<script setup lang="ts">

View File

@ -1,47 +1,32 @@
import { invoke } from "@tauri-apps/api/core";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { join, resourceDir } from "@tauri-apps/api/path";
import { readTextFile, readDir } from "@tauri-apps/plugin-fs";
import { convertFileSrc } from "@tauri-apps/api/core";
import { exists, readDir, readTextFile, remove } from "@tauri-apps/plugin-fs";
import { and, eq } from "drizzle-orm";
import type {
IHaexHubExtension,
IHaexHubExtensionLink,
IHaexHubExtensionManifest,
} from "~/types/haexhub";
import { haexExtensions } from "~~/src-tauri/database/schemas/vault";
import { eq } from "drizzle-orm";
const manifestFileName = "manifest.json";
const logoFileName = "logo.svg";
export interface IHaexHubExtensionLink {
name: string;
icon: string;
tooltip?: string;
id: string;
version: string;
manifest?: IHaexHubExtensionManifest;
}
export interface IHaexHubExtensionManifest {
name: string;
id: string;
entry: string;
author: string;
url: string;
version: string;
icon: string;
permissions: {
database?: {
read?: string[];
write?: string[];
create?: string[];
};
http?: string[];
filesystem?: {
read?: string[];
write?: string[];
};
};
}
export const useExtensionsStore = defineStore("extensionsStore", () => {
const availableExtensions = ref<IHaexHubExtensionLink[]>([]);
const extensionLinks = computed<ISidebarItem[]>(() =>
availableExtensions.value
.filter((extension) => extension.enabled && extension.installed)
.map((extension) => ({
icon: extension.icon ?? "",
id: extension.id,
name: extension.name ?? "",
tooltip: extension.name ?? "",
to: { name: "haexExtension", params: { extensionId: extension.id } },
}))
);
const currentRoute = useRouter().currentRoute;
const isActive = (id: string) =>
@ -82,6 +67,16 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
}
};
const isExtensionInstalledAsync = async (extension: Partial<IHaexHubExtension>) => {
try {
const extensionPath = await getExtensionPathAsync(extension.id, `${extension.version}`);
console.log(`extension ${extension.id} is installed ${await exists(extensionPath)}`);
return await exists(extensionPath);
} catch (error) {
return false;
}
};
const checkManifest = (manifestFile: unknown): manifestFile is IHaexHubExtensionManifest => {
const errors = [];
@ -130,8 +125,10 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
return true;
};
const readManifestFileAsync = async (extensionId: string, version: string | number) => {
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(
@ -139,7 +136,7 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
)) as IHaexHubExtensionManifest;
/*
TODO implement await checkManifets(manifest);
TODO implement check, that manifest has valid data
*/
return manifest;
} catch (error) {
@ -287,77 +284,83 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
const extensionEntry = computedAsync(
async () => {
console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log("extension id or entry missing", currentExtension.value);
return "no mani: " + currentExtension.value;
}
//return "wadahadedzdz";
try {
console.log("extensionEntry start", currentExtension.value);
const regex = /((href|src)=["'])([^"']+)(["'])/g;
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);
return `asset://localhost/${entryPath}`;
let entryHtml = await readTextFile(entryPath);
console.log("entryHtml", entryHtml);
const replacements = [];
let match;
while ((match = regex.exec(entryHtml)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith("http")) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
if (!currentExtension.value?.id || !currentExtension.value.version) {
console.log("extension id or entry missing", currentExtension.value);
return "no mani: " + currentExtension.value;
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const fileContent = await readTextFile(await join(extensionPath, resource));
const blob = new Blob([fileContent], { type: getMimeType(resource) });
const blobUrl = URL.createObjectURL(blob);
console.log("blob", resource, blobUrl);
entryHtml = entryHtml.replace(match, `${prefix}${blobUrl}${suffix}`);
}
const extensionPath = await getExtensionPathAsync(
currentExtension.value?.id,
currentExtension.value?.version
); //await join(await resourceDir(), currentExtension.value.. extensionDir, entryFileName);
console.log("entryHtml", entryHtml);
console.log("extensionEntry extensionPath", extensionPath);
const manifest = await readManifestFileAsync(
currentExtension.value.id,
currentExtension.value.version
);
const blob = new Blob([entryHtml], { type: "text/html" });
const iframeSrc = URL.createObjectURL(blob);
if (!manifest) return "no manifest readable";
console.log("iframeSrc", iframeSrc);
const entryPath = await join(extensionPath, manifest.entry);
/* const path = convertFileSrc(extensionDir, manifest.entry);
const hexName = stringToHex(
JSON.stringify({
id: currentExtension.value.id,
version: currentExtension.value.version,
})
);
return `haex-extension://${hexName}/index.html`;
return convertFileSrc(entryPath); //`asset://localhost/${entryPath}`;
let entryHtml = await readTextFile(entryPath);
console.log("entryHtml", entryHtml);
const replacements = [];
let match;
while ((match = regex.exec(entryHtml)) !== null) {
const [fullMatch, prefix, attr, resource, suffix] = match;
if (!resource.startsWith("http")) {
replacements.push({ match: fullMatch, resource, prefix, suffix });
}
}
for (const { match, resource, prefix, suffix } of replacements) {
const srcFile = convertFileSrc(await join(extensionPath, resource));
entryHtml = entryHtml.replace(match, `${prefix}${srcFile}${suffix}`);
}
console.log("entryHtml", entryHtml);
const blob = new Blob([entryHtml], { type: "text/html" });
const iframeSrc = URL.createObjectURL(blob);
console.log("iframeSrc", iframeSrc);
/* const path = convertFileSrc(extensionDir, manifest.entry);
console.log("final path", path); */
//manifest.entry = iframeSrc;
return iframeSrc;
/* await join(
//manifest.entry = iframeSrc;
return iframeSrc;
/* await join(
path, //`file:/${extensionDirectory}`,
manifest.entry
); */
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
// Modul-Datei laden
//const modulePathFull = await join(basePath, manifest.main);
/* const manifest: PluginManifest = await invoke('load_plugin', {
manifestPath,
}); */
/* const iframe = document.createElement('iframe');
/* const iframe = document.createElement('iframe');
iframe.src = manifest.entry;
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none'; */
/* const addonApi = {
/* const addonApi = {
db_execute: async (sql: string, params: string[] = []) => {
return invoke('db_execute', {
addonId: manifest.name,
@ -373,24 +376,27 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
});
},
}; */
/* iframe.onload = () => {
/* iframe.onload = () => {
iframe.contentWindow?.postMessage(
{ type: 'init', payload: addonApi },
'*'
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
);
};
window.addEventListener('message', (event) => {
if (event.source === iframe.contentWindow) {
const { type } = event.data;
if (type === 'ready') {
console.log(`Plugin ${manifest.name} ist bereit`);
}
}
}); */
/* plugins.value.push({ name: manifest.name, entry: manifest.entry });
console.log(`Plugin ${manifest.name} geladen.`); */
} catch (error) {
console.error("ERROR extensionEntry", error);
}
},
null,
{ lazy: true }
@ -399,28 +405,36 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
const loadExtensionsAsync = async () => {
const { currentVault } = storeToRefs(useVaultStore());
/* const query = db
.select()
.from(haexExtensions)
//.where(sql`${haexExtensions.enabled} = "1"`);
.where(eq(haexExtensions.enabled, true)); */
const extensions = await currentVault.value?.drizzle
.select()
.from(haexExtensions)
.where(eq(haexExtensions.enabled, true));
const extensions = (await currentVault.value?.drizzle.select().from(haexExtensions)) ?? [];
//if (!extensions?.length) return false;
const installedExtensions = await filterAsync(extensions, isExtensionInstalledAsync);
console.log("loadExtensionsAsync installedExtensions", installedExtensions);
//const manifest = readTextFile(manifestFileName)
//const { sql, params } = query.toSQL();
//const extensions = await invoke<any>("sql_select", { sql, params });
console.log("loadExtensionsAsync ", extensions);
availableExtensions.value =
extensions?.map((extension) => ({
extensions.map((extension) => ({
id: extension.id,
name: extension.name ?? "",
icon: extension.icon ?? "",
tooltip: extension.name ?? "",
author: extension.author ?? "",
version: extension.version ?? "",
enabled: extension.enabled ? true : false,
installed: installedExtensions.includes(extension),
})) ?? [];
console.log("loadExtensionsAsync", availableExtensions.value);
return true;
};
const removeExtensionAsync = async (id: string, version: string) => {
try {
console.log("remove extension", id, version);
await removeExtensionFromVaultAsync(id, version);
await removeExtensionFilesAsync(id, version);
} catch (error) {
throw new Error(JSON.stringify(error));
}
};
return {
@ -428,10 +442,13 @@ export const useExtensionsStore = defineStore("extensionsStore", () => {
checkManifest,
currentExtension,
extensionEntry,
extensionLinks,
installAsync,
isActive,
loadExtensionsAsync,
readManifestFileAsync,
removeExtensionAsync,
getExtensionPathAsync,
};
});
@ -440,3 +457,36 @@ const getMimeType = (file: string) => {
if (file.endsWith(".js")) return "text/javascript";
return "text/plain";
};
const removeExtensionFromVaultAsync = async (id: string | null, version: string | null) => {
if (!id) throw new Error("Erweiterung kann nicht gelöscht werden. Es keine ID angegeben");
if (!version)
throw new Error("Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben");
const { currentVault } = useVaultStore();
const removedExtensions = await currentVault?.drizzle
.delete(haexExtensions)
.where(and(eq(haexExtensions.id, id), eq(haexExtensions.version, version)));
return removedExtensions;
};
const removeExtensionFilesAsync = async (id: string | null, version: string | null) => {
try {
const { getExtensionPathAsync } = useExtensionsStore();
if (!id) throw new Error("Erweiterung kann nicht gelöscht werden. Es keine ID angegeben");
if (!version)
throw new Error("Erweiterung kann nicht gelöscht werden. Es wurde keine Version angegeben");
const extensionDirectory = await getExtensionPathAsync(id, version);
await remove(extensionDirectory, {
recursive: true,
});
} catch (error) {
console.error("ERROR removeExtensionFilesAsync", error);
throw new Error(JSON.stringify(error));
}
};
const replaceUrlWithAssetProtocolAsync = () => { };

View File

@ -1,5 +1,4 @@
import { getSingleRouteParam } from "~/composables/helper";
import type { RouteLocationRaw, RouteLocationAsRelativeGeneric } from "vue-router";
import type { RouteLocationAsRelativeGeneric } from "vue-router";
export interface ISidebarItem {
name: string;
@ -7,6 +6,7 @@ export interface ISidebarItem {
tooltip?: string;
id: string;
to?: RouteLocationAsRelativeGeneric;
iconType?: "icon" | "svg";
}
export const useSidebarStore = defineStore("sidebarStore", () => {

View File

@ -4,18 +4,15 @@ import { drizzle, SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
import * as schema from "@/../src-tauri/database/schemas/vault";
import { invoke } from "@tauri-apps/api/core";
import { count } from "drizzle-orm";
import { and, count, eq } from "drizzle-orm";
import { platform } from "@tauri-apps/plugin-os";
interface IVault {
//database: Database;
path: string;
password: string;
name: string;
drizzle: SqliteRemoteDatabase<typeof schema>;
}
interface IOpenVaults {
[vaultPath: string]: IVault;
[vaultId: string]: IVault;
}
export const useVaultStore = defineStore("vaultStore", () => {
@ -58,27 +55,20 @@ export const useVaultStore = defineStore("vaultStore", () => {
const openAsync = async ({ path = "", password }: { path: string; password: string }) => {
try {
console.log("try to open db", path, password);
const result = await invoke<string>("open_encrypted_database", {
path,
key: password,
});
console.log("open vault from store", result);
//const db = await Database.load(sqlitePath);
if (result !== "success") throw new Error(result);
const vaultId = await getVaultIdAsync(path);
const seperator = platform() === "windows" ? "\\" : "/";
const fileName = path.split(seperator).pop();
console.log("opened db fileName", fileName, vaultId);
openVaults.value = {
...openVaults.value,
[vaultId]: {
//database: db,
path,
password,
name: fileName ?? path,
drizzle: drizzle<typeof schema>(
async (sql, params: unknown[], method) => {
@ -102,24 +92,15 @@ export const useVaultStore = defineStore("vaultStore", () => {
return { rows: [] };
}
/* rows = rows.map((row: any) => {
return Object.values(row);
}); */
console.log("select after map", rows);
// If the method is "all", return all rows
results = method === "all" ? rows : rows[0];
return { rows: results };
},
// Pass the schema to the drizzle instance
{ schema: schema, logger: true }
),
},
};
//if (!(await testDatabaseReadAsync())) throw new Error("Passwort falsch");
const { addVaultAsync } = useLastVaultStore();
await addVaultAsync({ path });
@ -127,15 +108,6 @@ export const useVaultStore = defineStore("vaultStore", () => {
} catch (error) {
console.error("Error openAsync ", error);
return false;
//if (error === "file is not a database") throw new Error("Passwort falsch");
}
};
const testDatabaseReadAsync = async () => {
try {
return currentVault.value?.drizzle.select({ count: count() }).from(schema.haexExtensions);
} catch (error) {
return false;
}
};
@ -198,7 +170,7 @@ const getVaultIdAsync = async (path: string) => {
return hashHex;
};
function isSelectQuery(sql: string): boolean {
const isSelectQuery = (sql: string) => {
const selectRegex = /^\s*SELECT\b/i;
return selectRegex.test(sql);
}
};

View File

@ -1,6 +1,11 @@
export interface IHaexHubExtensionManifest {
name: string;
id: string;
entry: string;
author: string;
url: string;
version: string;
icon: string;
permissions: {
database?: {
read?: string[];
@ -14,3 +19,17 @@ export interface IHaexHubExtensionManifest {
};
};
}
export interface IHaexHubExtensionLink extends IHaexHubExtension {
installed: boolean;
}
export interface IHaexHubExtension {
author?: string | null;
enabled?: boolean | null;
icon?: string | null;
id: string;
manifest?: IHaexHubExtensionManifest;
name: string | null;
version?: string | null;
}