mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
fix window on workspace rendering
This commit is contained in:
@ -156,11 +156,11 @@ export const haexDesktopItems = sqliteTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||||
enum: ['extension', 'file', 'folder'],
|
enum: ['system', 'extension', 'file', 'folder'],
|
||||||
}).notNull(),
|
}).notNull(),
|
||||||
referenceId: text(
|
referenceId: text(
|
||||||
tableNames.haex.desktop_items.columns.referenceId,
|
tableNames.haex.desktop_items.columns.referenceId,
|
||||||
).notNull(), // extensionId für extensions, filePath für files/folders
|
).notNull(), // systemId für system windows, extensionId für extensions, filePath für files/folders
|
||||||
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
|
|||||||
@ -88,30 +88,64 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot extract ZIP: {}", e),
|
reason: format!("Cannot extract ZIP: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Check if manifest.json is directly in temp or in a subdirectory
|
// Read haextension_dir from config if it exists, otherwise use default
|
||||||
let manifest_path = temp.join("manifest.json");
|
let config_path = temp.join("haextension.config.json");
|
||||||
let actual_dir = if manifest_path.exists() {
|
let haextension_dir = if config_path.exists() {
|
||||||
temp.clone()
|
let config_content = std::fs::read_to_string(&config_path)
|
||||||
} else {
|
.map_err(|e| ExtensionError::ManifestError {
|
||||||
// manifest.json is in a subdirectory - find it
|
reason: format!("Cannot read haextension.config.json: {}", e),
|
||||||
let mut found_dir = None;
|
})?;
|
||||||
for entry in fs::read_dir(&temp)
|
|
||||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?
|
let config: serde_json::Value = serde_json::from_str(&config_content)
|
||||||
{
|
.map_err(|e| ExtensionError::ManifestError {
|
||||||
let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
reason: format!("Invalid haextension.config.json: {}", e),
|
||||||
let path = entry.path();
|
})?;
|
||||||
if path.is_dir() && path.join("manifest.json").exists() {
|
|
||||||
found_dir = Some(path);
|
let dir = config
|
||||||
break;
|
.get("dev")
|
||||||
}
|
.and_then(|dev| dev.get("haextension_dir"))
|
||||||
|
.and_then(|dir| dir.as_str())
|
||||||
|
.unwrap_or("haextension")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Security: Validate that haextension_dir doesn't contain ".." for path traversal
|
||||||
|
if dir.contains("..") {
|
||||||
|
return Err(ExtensionError::ManifestError {
|
||||||
|
reason: "Invalid haextension_dir: path traversal with '..' not allowed".to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
found_dir.ok_or_else(|| ExtensionError::ManifestError {
|
dir
|
||||||
reason: "manifest.json not found in extension archive".to_string(),
|
} else {
|
||||||
})?
|
"haextension".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let manifest_path = actual_dir.join("manifest.json");
|
// Build the manifest path
|
||||||
|
let manifest_path = temp.join(&haextension_dir).join("manifest.json");
|
||||||
|
|
||||||
|
// Ensure the resolved path is still within temp directory (safety check against path traversal)
|
||||||
|
let canonical_temp = temp.canonicalize()
|
||||||
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
|
|
||||||
|
// Only check if manifest_path parent exists to avoid errors
|
||||||
|
if let Some(parent) = manifest_path.parent() {
|
||||||
|
if let Ok(canonical_manifest_dir) = parent.canonicalize() {
|
||||||
|
if !canonical_manifest_dir.starts_with(&canonical_temp) {
|
||||||
|
return Err(ExtensionError::ManifestError {
|
||||||
|
reason: "Security violation: manifest path outside extension directory".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if manifest exists
|
||||||
|
if !manifest_path.exists() {
|
||||||
|
return Err(ExtensionError::ManifestError {
|
||||||
|
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual_dir = temp.clone();
|
||||||
let manifest_content =
|
let manifest_content =
|
||||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||||
reason: format!("Cannot read manifest: {}", e),
|
reason: format!("Cannot read manifest: {}", e),
|
||||||
@ -119,7 +153,7 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| {
|
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
||||||
ExtensionError::SignatureVerificationFailed {
|
ExtensionError::SignatureVerificationFailed {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,28 +4,13 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src-tauri/src/extension/crypto.rs
|
// src-tauri/src/extension/crypto.rs
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
pub struct ExtensionCrypto;
|
pub struct ExtensionCrypto;
|
||||||
|
|
||||||
impl ExtensionCrypto {
|
impl ExtensionCrypto {
|
||||||
/// Berechnet Hash vom Public Key (wie im SDK)
|
|
||||||
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
|
|
||||||
let public_key_bytes =
|
|
||||||
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
|
|
||||||
|
|
||||||
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
|
|
||||||
.map_err(|e| format!("Invalid public key: {}", e))?;
|
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(public_key.as_bytes());
|
|
||||||
let result = hasher.finalize();
|
|
||||||
|
|
||||||
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
|
|
||||||
Ok(hex::encode(&result[..10]))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifiziert Extension-Signatur
|
/// Verifiziert Extension-Signatur
|
||||||
pub fn verify_signature(
|
pub fn verify_signature(
|
||||||
public_key_hex: &str,
|
public_key_hex: &str,
|
||||||
@ -50,26 +35,48 @@ impl ExtensionCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
||||||
pub fn hash_directory(dir: &Path) -> Result<String, String> {
|
pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result<String, ExtensionError> {
|
||||||
// 1. Alle Dateipfade rekursiv sammeln
|
// 1. Alle Dateipfade rekursiv sammeln
|
||||||
let mut all_files = Vec::new();
|
let mut all_files = Vec::new();
|
||||||
Self::collect_files_recursively(dir, &mut all_files)
|
Self::collect_files_recursively(dir, &mut all_files)
|
||||||
.map_err(|e| format!("Failed to collect files: {}", e))?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
all_files.sort();
|
|
||||||
|
// 2. Konvertiere zu relativen Pfaden für konsistente Sortierung (wie im SDK)
|
||||||
|
let mut relative_files: Vec<(String, PathBuf)> = all_files
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let relative = path.strip_prefix(dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
(relative, path)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// 3. Sortiere nach relativen Pfaden
|
||||||
|
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
println!("=== Files to hash ({}): ===", relative_files.len());
|
||||||
|
for (rel, _) in &relative_files {
|
||||||
|
println!(" - {}", rel);
|
||||||
|
}
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
let manifest_path = dir.join("manifest.json");
|
|
||||||
|
|
||||||
// 2. Inhalte der sortierten Dateien hashen
|
// 4. Inhalte der sortierten Dateien hashen
|
||||||
for file_path in all_files {
|
for (_relative, file_path) in relative_files {
|
||||||
if file_path == manifest_path {
|
if file_path == manifest_path {
|
||||||
// FÜR DIE MANIFEST.JSON:
|
// FÜR DIE MANIFEST.JSON:
|
||||||
let content_str = fs::read_to_string(&file_path)
|
let content_str = fs::read_to_string(&file_path)
|
||||||
.map_err(|e| format!("Cannot read manifest file: {}", e))?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
|
|
||||||
// Parse zu einem generischen JSON-Wert
|
// Parse zu einem generischen JSON-Wert
|
||||||
let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
|
let mut manifest: serde_json::Value =
|
||||||
.map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
|
serde_json::from_str(&content_str).map_err(|e| {
|
||||||
|
ExtensionError::ManifestError {
|
||||||
|
reason: format!("Cannot parse manifest JSON: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
|
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
|
||||||
if let Some(obj) = manifest.as_object_mut() {
|
if let Some(obj) = manifest.as_object_mut() {
|
||||||
@ -80,13 +87,19 @@ impl ExtensionCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
|
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
|
||||||
let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap();
|
// serde_json sortiert die Keys automatisch alphabetisch
|
||||||
|
let canonical_manifest_content =
|
||||||
|
serde_json::to_string_pretty(&manifest).map_err(|e| {
|
||||||
|
ExtensionError::ManifestError {
|
||||||
|
reason: format!("Failed to serialize manifest: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
||||||
hasher.update(canonical_manifest_content.as_bytes());
|
hasher.update(canonical_manifest_content.as_bytes());
|
||||||
} else {
|
} else {
|
||||||
// FÜR ALLE ANDEREN DATEIEN:
|
// FÜR ALLE ANDEREN DATEIEN:
|
||||||
let content = fs::read(&file_path)
|
let content =
|
||||||
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
|
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
hasher.update(&content);
|
hasher.update(&content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -223,6 +223,16 @@ pub fn is_extension_installed(
|
|||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct HaextensionConfig {
|
struct HaextensionConfig {
|
||||||
dev: DevConfig,
|
dev: DevConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
keys: KeysConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Default)]
|
||||||
|
struct KeysConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
public_key_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
private_key_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
@ -231,6 +241,8 @@ struct DevConfig {
|
|||||||
port: u16,
|
port: u16,
|
||||||
#[serde(default = "default_host")]
|
#[serde(default = "default_host")]
|
||||||
host: String,
|
host: String,
|
||||||
|
#[serde(default = "default_haextension_dir")]
|
||||||
|
haextension_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_port() -> u16 {
|
fn default_port() -> u16 {
|
||||||
@ -241,6 +253,10 @@ fn default_host() -> String {
|
|||||||
"localhost".to_string()
|
"localhost".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_haextension_dir() -> String {
|
||||||
|
"haextension".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a dev server is reachable by making a simple HTTP request
|
/// Check if a dev server is reachable by making a simple HTTP request
|
||||||
async fn check_dev_server_health(url: &str) -> bool {
|
async fn check_dev_server_health(url: &str) -> bool {
|
||||||
use tauri_plugin_http::reqwest;
|
use tauri_plugin_http::reqwest;
|
||||||
@ -276,29 +292,30 @@ pub async fn load_dev_extension(
|
|||||||
|
|
||||||
let extension_path_buf = PathBuf::from(&extension_path);
|
let extension_path_buf = PathBuf::from(&extension_path);
|
||||||
|
|
||||||
// 1. Read haextension.json to get dev server config
|
// 1. Read haextension.config.json to get dev server config and haextension directory
|
||||||
let config_path = extension_path_buf.join("haextension.json");
|
let config_path = extension_path_buf.join("haextension.config.json");
|
||||||
let (host, port) = if config_path.exists() {
|
let (host, port, haextension_dir) = if config_path.exists() {
|
||||||
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
|
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
|
||||||
ExtensionError::ValidationError {
|
ExtensionError::ValidationError {
|
||||||
reason: format!("Failed to read haextension.json: {}", e),
|
reason: format!("Failed to read haextension.config.json: {}", e),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
|
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
|
||||||
ExtensionError::ValidationError {
|
ExtensionError::ValidationError {
|
||||||
reason: format!("Failed to parse haextension.json: {}", e),
|
reason: format!("Failed to parse haextension.config.json: {}", e),
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
(config.dev.host, config.dev.port)
|
(config.dev.host, config.dev.port, config.dev.haextension_dir)
|
||||||
} else {
|
} else {
|
||||||
// Default values if config doesn't exist
|
// Default values if config doesn't exist
|
||||||
(default_host(), default_port())
|
(default_host(), default_port(), default_haextension_dir())
|
||||||
};
|
};
|
||||||
|
|
||||||
let dev_server_url = format!("http://{}:{}", host, port);
|
let dev_server_url = format!("http://{}:{}", host, port);
|
||||||
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
||||||
|
eprintln!("📁 Haextension directory: {}", haextension_dir);
|
||||||
|
|
||||||
// 1.5. Check if dev server is running
|
// 1.5. Check if dev server is running
|
||||||
if !check_dev_server_health(&dev_server_url).await {
|
if !check_dev_server_health(&dev_server_url).await {
|
||||||
@ -311,8 +328,8 @@ pub async fn load_dev_extension(
|
|||||||
}
|
}
|
||||||
eprintln!("✅ Dev server is reachable");
|
eprintln!("✅ Dev server is reachable");
|
||||||
|
|
||||||
// 2. Build path to manifest: <extension_path>/haextension/manifest.json
|
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||||
let manifest_path = extension_path_buf.join("haextension").join("manifest.json");
|
let manifest_path = extension_path_buf.join(&haextension_dir).join("manifest.json");
|
||||||
|
|
||||||
// Check if manifest exists
|
// Check if manifest exists
|
||||||
if !manifest_path.exists() {
|
if !manifest_path.exists() {
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
:alt="label"
|
:alt="label"
|
||||||
class="w-14 h-14 object-contain transition-transform duration-200"
|
class="w-14 h-14 object-contain transition-transform duration-200"
|
||||||
:class="{ 'scale-110': isSelected }"
|
:class="{ 'scale-110': isSelected }"
|
||||||
>
|
/>
|
||||||
<UIcon
|
<UIcon
|
||||||
v-else
|
v-else
|
||||||
name="i-heroicons-puzzle-piece-solid"
|
name="i-heroicons-puzzle-piece-solid"
|
||||||
@ -69,7 +69,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
itemType: 'extension' | 'file' | 'folder'
|
itemType: DesktopItemType
|
||||||
referenceId: string
|
referenceId: string
|
||||||
initialX: number
|
initialX: number
|
||||||
initialY: number
|
initialY: number
|
||||||
|
|||||||
@ -27,6 +27,8 @@
|
|||||||
class="w-full h-full relative isolate"
|
class="w-full h-full relative isolate"
|
||||||
@click.self.stop="handleDesktopClick"
|
@click.self.stop="handleDesktopClick"
|
||||||
@mousedown.left.self="handleAreaSelectStart"
|
@mousedown.left.self="handleAreaSelectStart"
|
||||||
|
@dragover.prevent="handleDragOver"
|
||||||
|
@drop.prevent="handleDrop($event, workspace.id)"
|
||||||
>
|
>
|
||||||
<!-- Grid Pattern Background -->
|
<!-- Grid Pattern Background -->
|
||||||
<div
|
<div
|
||||||
@ -81,128 +83,113 @@
|
|||||||
v-for="window in getWorkspaceWindows(workspace.id)"
|
v-for="window in getWorkspaceWindows(workspace.id)"
|
||||||
:key="window.id"
|
:key="window.id"
|
||||||
>
|
>
|
||||||
<!-- Desktop container for when overview is closed -->
|
<!-- Overview Mode: Teleport to window preview -->
|
||||||
<div
|
|
||||||
:id="`desktop-container-${window.id}`"
|
|
||||||
class="absolute"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Window with dynamic teleport -->
|
|
||||||
<Teleport
|
<Teleport
|
||||||
:to="
|
v-if="windowManager.showWindowOverview && overviewWindowState.has(window.id)"
|
||||||
windowManager.showWindowOverview &&
|
:to="`#window-preview-${window.id}`"
|
||||||
overviewWindowState.has(window.id)
|
|
||||||
? `#window-preview-${window.id}`
|
|
||||||
: `#desktop-container-${window.id}`
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template
|
<div
|
||||||
v-if="
|
class="absolute origin-top-left"
|
||||||
windowManager.showWindowOverview &&
|
:style="{
|
||||||
overviewWindowState.has(window.id)
|
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||||
"
|
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||||
|
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<HaexWindow
|
||||||
class="absolute origin-top-left"
|
v-show="
|
||||||
:style="{
|
windowManager.showWindowOverview || !window.isMinimized
|
||||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
"
|
||||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
:id="window.id"
|
||||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||||
}"
|
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||||
|
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||||
|
v-model:height="overviewWindowState.get(window.id)!.height"
|
||||||
|
:title="window.title"
|
||||||
|
:icon="window.icon"
|
||||||
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
|
:source-x="window.sourceX"
|
||||||
|
:source-y="window.sourceY"
|
||||||
|
:source-width="window.sourceWidth"
|
||||||
|
:source-height="window.sourceHeight"
|
||||||
|
:is-opening="window.isOpening"
|
||||||
|
:is-closing="window.isClosing"
|
||||||
|
class="no-swipe"
|
||||||
|
@close="windowManager.closeWindow(window.id)"
|
||||||
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
|
@position-changed="
|
||||||
|
(x, y) =>
|
||||||
|
windowManager.updateWindowPosition(window.id, x, y)
|
||||||
|
"
|
||||||
|
@size-changed="
|
||||||
|
(width, height) =>
|
||||||
|
windowManager.updateWindowSize(window.id, width, height)
|
||||||
|
"
|
||||||
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
|
@drag-end="handleWindowDragEnd"
|
||||||
>
|
>
|
||||||
<HaexWindow
|
<!-- System Window: Render Vue Component -->
|
||||||
v-show="
|
<component
|
||||||
windowManager.showWindowOverview || !window.isMinimized
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
"
|
v-if="window.type === 'system'"
|
||||||
:id="window.id"
|
/>
|
||||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
|
||||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
|
||||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
|
||||||
v-model:height="overviewWindowState.get(window.id)!.height"
|
|
||||||
:title="window.title"
|
|
||||||
:icon="window.icon"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
|
||||||
:source-x="window.sourceX"
|
|
||||||
:source-y="window.sourceY"
|
|
||||||
:source-width="window.sourceWidth"
|
|
||||||
:source-height="window.sourceHeight"
|
|
||||||
:is-opening="window.isOpening"
|
|
||||||
:is-closing="window.isClosing"
|
|
||||||
class="no-swipe"
|
|
||||||
@close="windowManager.closeWindow(window.id)"
|
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
|
||||||
@position-changed="
|
|
||||||
(x, y) =>
|
|
||||||
windowManager.updateWindowPosition(window.id, x, y)
|
|
||||||
"
|
|
||||||
@size-changed="
|
|
||||||
(width, height) =>
|
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
|
||||||
"
|
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
|
||||||
@drag-end="handleWindowDragEnd"
|
|
||||||
>
|
|
||||||
<!-- System Window: Render Vue Component -->
|
|
||||||
<component
|
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
|
||||||
v-if="window.type === 'system'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
<!-- Extension Window: Render iFrame -->
|
||||||
<HaexDesktopExtensionFrame
|
<HaexDesktopExtensionFrame
|
||||||
v-else
|
v-else
|
||||||
:extension-id="window.sourceId"
|
:extension-id="window.sourceId"
|
||||||
:window-id="window.id"
|
:window-id="window.id"
|
||||||
/>
|
/>
|
||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<HaexWindow
|
|
||||||
v-else
|
|
||||||
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
|
||||||
:id="window.id"
|
|
||||||
v-model:x="window.x"
|
|
||||||
v-model:y="window.y"
|
|
||||||
v-model:width="window.width"
|
|
||||||
v-model:height="window.height"
|
|
||||||
:title="window.title"
|
|
||||||
:icon="window.icon"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
|
||||||
:source-x="window.sourceX"
|
|
||||||
:source-y="window.sourceY"
|
|
||||||
:source-width="window.sourceWidth"
|
|
||||||
:source-height="window.sourceHeight"
|
|
||||||
:is-opening="window.isOpening"
|
|
||||||
:is-closing="window.isClosing"
|
|
||||||
class="no-swipe"
|
|
||||||
@close="windowManager.closeWindow(window.id)"
|
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
|
||||||
@position-changed="
|
|
||||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
|
||||||
"
|
|
||||||
@size-changed="
|
|
||||||
(width, height) =>
|
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
|
||||||
"
|
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
|
||||||
@drag-end="handleWindowDragEnd"
|
|
||||||
>
|
|
||||||
<!-- System Window: Render Vue Component -->
|
|
||||||
<component
|
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
|
||||||
v-if="window.type === 'system'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
|
||||||
<HaexDesktopExtensionFrame
|
|
||||||
v-else
|
|
||||||
:extension-id="window.sourceId"
|
|
||||||
:window-id="window.id"
|
|
||||||
/>
|
|
||||||
</HaexWindow>
|
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- Desktop Mode: Render directly in workspace -->
|
||||||
|
<HaexWindow
|
||||||
|
v-else
|
||||||
|
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||||
|
:id="window.id"
|
||||||
|
v-model:x="window.x"
|
||||||
|
v-model:y="window.y"
|
||||||
|
v-model:width="window.width"
|
||||||
|
v-model:height="window.height"
|
||||||
|
:title="window.title"
|
||||||
|
:icon="window.icon"
|
||||||
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
|
:source-x="window.sourceX"
|
||||||
|
:source-y="window.sourceY"
|
||||||
|
:source-width="window.sourceWidth"
|
||||||
|
:source-height="window.sourceHeight"
|
||||||
|
:is-opening="window.isOpening"
|
||||||
|
:is-closing="window.isClosing"
|
||||||
|
class="no-swipe"
|
||||||
|
@close="windowManager.closeWindow(window.id)"
|
||||||
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
|
@position-changed="
|
||||||
|
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
||||||
|
"
|
||||||
|
@size-changed="
|
||||||
|
(width, height) =>
|
||||||
|
windowManager.updateWindowSize(window.id, width, height)
|
||||||
|
"
|
||||||
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
|
@drag-end="handleWindowDragEnd"
|
||||||
|
>
|
||||||
|
<!-- System Window: Render Vue Component -->
|
||||||
|
<component
|
||||||
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
|
v-if="window.type === 'system'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Extension Window: Render iFrame -->
|
||||||
|
<HaexDesktopExtensionFrame
|
||||||
|
v-else
|
||||||
|
:extension-id="window.sourceId"
|
||||||
|
:window-id="window.id"
|
||||||
|
/>
|
||||||
|
</HaexWindow>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
@ -342,6 +329,18 @@ const getWorkspaceIcons = (workspaceId: string) => {
|
|||||||
return desktopItems.value
|
return desktopItems.value
|
||||||
.filter((item) => item.workspaceId === workspaceId)
|
.filter((item) => item.workspaceId === workspaceId)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
|
if (item.itemType === 'system') {
|
||||||
|
const systemWindow = windowManager.getAllSystemWindows().find(
|
||||||
|
(win) => win.id === item.referenceId,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
label: systemWindow?.name || 'Unknown',
|
||||||
|
icon: systemWindow?.icon || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (item.itemType === 'extension') {
|
if (item.itemType === 'extension') {
|
||||||
const extension = availableExtensions.value.find(
|
const extension = availableExtensions.value.find(
|
||||||
(ext) => ext.id === item.referenceId,
|
(ext) => ext.id === item.referenceId,
|
||||||
@ -416,6 +415,49 @@ const handleDragEnd = async () => {
|
|||||||
allowSwipe.value = true // Re-enable Swiper after drag
|
allowSwipe.value = true // Re-enable Swiper after drag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle drag over for launcher items
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
if (!event.dataTransfer) return
|
||||||
|
|
||||||
|
// Check if this is a launcher item
|
||||||
|
if (event.dataTransfer.types.includes('application/haex-launcher-item')) {
|
||||||
|
event.dataTransfer.dropEffect = 'copy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drop for launcher items
|
||||||
|
const handleDrop = async (event: DragEvent, workspaceId: string) => {
|
||||||
|
if (!event.dataTransfer) return
|
||||||
|
|
||||||
|
const launcherItemData = event.dataTransfer.getData('application/haex-launcher-item')
|
||||||
|
if (!launcherItemData) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = JSON.parse(launcherItemData) as {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
type: 'system' | 'extension'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get drop position relative to desktop
|
||||||
|
const desktopRect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||||
|
const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
||||||
|
const y = Math.max(0, event.clientY - desktopRect.top - 32)
|
||||||
|
|
||||||
|
// Create desktop icon on the specific workspace
|
||||||
|
await desktopStore.addDesktopItemAsync(
|
||||||
|
item.type as DesktopItemType,
|
||||||
|
item.id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
workspaceId
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create desktop icon:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDesktopClick = () => {
|
const handleDesktopClick = () => {
|
||||||
// Only clear selection if it was a simple click, not an area selection
|
// Only clear selection if it was a simple click, not an area selection
|
||||||
// Check if we just finished an area selection (box size > threshold)
|
// Check if we just finished an area selection (box size > threshold)
|
||||||
|
|||||||
@ -11,22 +11,29 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
||||||
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
||||||
<UiButton
|
<UContextMenu
|
||||||
v-for="item in launcherItems"
|
v-for="item in launcherItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
square
|
:items="getContextMenuItems(item)"
|
||||||
size="xl"
|
>
|
||||||
variant="ghost"
|
<UiButton
|
||||||
:ui="{
|
square
|
||||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
size="lg"
|
||||||
leadingIcon: 'size-10',
|
variant="ghost"
|
||||||
label: 'w-full',
|
:ui="{
|
||||||
}"
|
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
|
||||||
:icon="item.icon"
|
leadingIcon: 'size-10',
|
||||||
:label="item.name"
|
label: 'w-full',
|
||||||
:tooltip="item.name"
|
}"
|
||||||
@click="openItem(item)"
|
:icon="item.icon"
|
||||||
/>
|
:label="item.name"
|
||||||
|
:tooltip="item.name"
|
||||||
|
draggable="true"
|
||||||
|
@click="openItem(item)"
|
||||||
|
@dragstart="handleDragStart($event, item)"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
/>
|
||||||
|
</UContextMenu>
|
||||||
|
|
||||||
<!-- Disabled Extensions (grayed out) -->
|
<!-- Disabled Extensions (grayed out) -->
|
||||||
<UiButton
|
<UiButton
|
||||||
@ -119,14 +126,77 @@ const openItem = async (item: LauncherItem) => {
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uninstall extension
|
||||||
|
const uninstallExtension = async (item: LauncherItem) => {
|
||||||
|
try {
|
||||||
|
const extension = extensionStore.availableExtensions.find(ext => ext.id === item.id)
|
||||||
|
if (!extension) return
|
||||||
|
|
||||||
|
await extensionStore.removeExtensionAsync(
|
||||||
|
extension.publicKey,
|
||||||
|
extension.name,
|
||||||
|
extension.version
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to uninstall extension:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context menu items for launcher item
|
||||||
|
const getContextMenuItems = (item: LauncherItem) => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: t('contextMenu.open'),
|
||||||
|
icon: 'i-heroicons-arrow-top-right-on-square',
|
||||||
|
click: () => openItem(item),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add uninstall option for extensions
|
||||||
|
if (item.type === 'extension') {
|
||||||
|
items.push({
|
||||||
|
label: t('contextMenu.uninstall'),
|
||||||
|
icon: 'i-heroicons-trash',
|
||||||
|
click: () => uninstallExtension(item),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag & Drop handling
|
||||||
|
const handleDragStart = (event: DragEvent, item: LauncherItem) => {
|
||||||
|
if (!event.dataTransfer) return
|
||||||
|
|
||||||
|
// Store the launcher item data
|
||||||
|
event.dataTransfer.effectAllowed = 'copy'
|
||||||
|
event.dataTransfer.setData('application/haex-launcher-item', JSON.stringify(item))
|
||||||
|
|
||||||
|
// Set drag image (optional - uses default if not set)
|
||||||
|
const dragImage = event.target as HTMLElement
|
||||||
|
if (dragImage) {
|
||||||
|
event.dataTransfer.setDragImage(dragImage, 20, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
// Cleanup if needed
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
de:
|
de:
|
||||||
disabled: Deaktiviert
|
disabled: Deaktiviert
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
|
contextMenu:
|
||||||
|
open: Öffnen
|
||||||
|
uninstall: Deinstallieren
|
||||||
|
|
||||||
en:
|
en:
|
||||||
disabled: Disabled
|
disabled: Disabled
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
|
contextMenu:
|
||||||
|
open: Open
|
||||||
|
uninstall: Uninstall
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<!-- Window Thumbnails Flex Layout -->
|
<!-- Window Thumbnails Flex Layout -->
|
||||||
<div
|
<div
|
||||||
v-if="windows.length > 0"
|
v-if="windows.length > 0"
|
||||||
class="flex flex-wrap gap-6 justify-start items-start"
|
class="flex flex-wrap gap-6 justify-center-safe items-start"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="window in windows"
|
v-for="window in windows"
|
||||||
|
|||||||
@ -1,139 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="grid grid-rows-2 sm:grid-cols-2 sm:gap-2 p-2 max-w-2xl w-full h-fit"
|
|
||||||
>
|
|
||||||
<div class="p-2">{{ t('language') }}</div>
|
|
||||||
<div><UiDropdownLocale @select="onSelectLocaleAsync" /></div>
|
|
||||||
|
|
||||||
<div class="p-2">{{ t('design') }}</div>
|
|
||||||
<div><UiDropdownTheme @select="onSelectThemeAsync" /></div>
|
|
||||||
|
|
||||||
<div class="p-2">{{ t('vaultName.label') }}</div>
|
|
||||||
<div>
|
|
||||||
<UiInput
|
|
||||||
v-model="currentVaultName"
|
|
||||||
:placeholder="t('vaultName.label')"
|
|
||||||
@change="onSetVaultNameAsync"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-2">{{ t('notifications.label') }}</div>
|
|
||||||
<div>
|
|
||||||
<UiButton
|
|
||||||
:label="t('notifications.requestPermission')"
|
|
||||||
@click="requestNotificationPermissionAsync"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-2">{{ t('deviceName.label') }}</div>
|
|
||||||
<div>
|
|
||||||
<UiInput
|
|
||||||
v-model="deviceName"
|
|
||||||
:placeholder="t('deviceName.label')"
|
|
||||||
@change="onUpdateDeviceNameAsync"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Child routes (like developer.vue) will be rendered here -->
|
|
||||||
<NuxtPage />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Locale } from 'vue-i18n'
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
name: 'settings',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t, setLocale } = useI18n()
|
|
||||||
|
|
||||||
const { currentVaultName } = storeToRefs(useVaultStore())
|
|
||||||
const { updateVaultNameAsync, updateLocaleAsync, updateThemeAsync } =
|
|
||||||
useVaultSettingsStore()
|
|
||||||
|
|
||||||
const onSelectLocaleAsync = async (locale: Locale) => {
|
|
||||||
await updateLocaleAsync(locale)
|
|
||||||
await setLocale(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentThemeName } = storeToRefs(useUiStore())
|
|
||||||
|
|
||||||
const onSelectThemeAsync = async (theme: string) => {
|
|
||||||
currentThemeName.value = theme
|
|
||||||
console.log('onSelectThemeAsync', currentThemeName.value)
|
|
||||||
await updateThemeAsync(theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { add } = useToast()
|
|
||||||
|
|
||||||
const onSetVaultNameAsync = async () => {
|
|
||||||
try {
|
|
||||||
await updateVaultNameAsync(currentVaultName.value)
|
|
||||||
add({ description: t('vaultName.update.success'), color: 'success' })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
add({ description: t('vaultName.update.error'), color: 'error' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { requestNotificationPermissionAsync } = useNotificationStore()
|
|
||||||
|
|
||||||
const { deviceName } = storeToRefs(useDeviceStore())
|
|
||||||
const { updateDeviceNameAsync, readDeviceNameAsync } = useDeviceStore()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await readDeviceNameAsync()
|
|
||||||
})
|
|
||||||
|
|
||||||
const onUpdateDeviceNameAsync = async () => {
|
|
||||||
const check = vaultDeviceNameSchema.safeParse(deviceName.value)
|
|
||||||
if (!check.success) return
|
|
||||||
try {
|
|
||||||
await updateDeviceNameAsync({ name: deviceName.value })
|
|
||||||
add({ description: t('deviceName.update.success'), color: 'success' })
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
add({ description: t('deviceName.update.error'), color: 'error' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i18n lang="yaml">
|
|
||||||
de:
|
|
||||||
language: Sprache
|
|
||||||
design: Design
|
|
||||||
save: Änderung speichern
|
|
||||||
notifications:
|
|
||||||
label: Benachrichtigungen
|
|
||||||
requestPermission: Benachrichtigung erlauben
|
|
||||||
vaultName:
|
|
||||||
label: Vaultname
|
|
||||||
update:
|
|
||||||
success: Vaultname erfolgreich aktualisiert
|
|
||||||
error: Vaultname konnte nicht aktualisiert werden
|
|
||||||
deviceName:
|
|
||||||
label: Gerätename
|
|
||||||
update:
|
|
||||||
success: Gerätename wurde erfolgreich aktualisiert
|
|
||||||
error: Gerätename konnte nich aktualisiert werden
|
|
||||||
en:
|
|
||||||
language: Language
|
|
||||||
design: Design
|
|
||||||
save: save changes
|
|
||||||
notifications:
|
|
||||||
label: Notifications
|
|
||||||
requestPermission: Grant Permission
|
|
||||||
vaultName:
|
|
||||||
label: Vault Name
|
|
||||||
update:
|
|
||||||
success: Vault Name successfully updated
|
|
||||||
error: Vault name could not be updated
|
|
||||||
deviceName:
|
|
||||||
label: Device name
|
|
||||||
update:
|
|
||||||
success: Device name has been successfully updated
|
|
||||||
error: Device name could not be updated
|
|
||||||
</i18n>
|
|
||||||
@ -1,279 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="p-4 max-w-4xl mx-auto space-y-6">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<h1 class="text-2xl font-bold">{{ t('title') }}</h1>
|
|
||||||
<p class="text-sm opacity-70">{{ t('description') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add Dev Extension Form -->
|
|
||||||
<UCard class="p-4 space-y-4">
|
|
||||||
<h2 class="text-lg font-semibold">{{ t('add.title') }}</h2>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-sm font-medium">{{ t('add.extensionPath') }}</label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<UiInput
|
|
||||||
v-model="extensionPath"
|
|
||||||
:placeholder="t('add.extensionPathPlaceholder')"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<UiButton
|
|
||||||
:label="t('add.browse')"
|
|
||||||
variant="outline"
|
|
||||||
@click="browseExtensionPathAsync"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs opacity-60">{{ t('add.extensionPathHint') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UiButton
|
|
||||||
:label="t('add.loadExtension')"
|
|
||||||
:loading="isLoading"
|
|
||||||
:disabled="!extensionPath"
|
|
||||||
@click="loadDevExtensionAsync"
|
|
||||||
/>
|
|
||||||
</UCard>
|
|
||||||
|
|
||||||
<!-- List of Dev Extensions -->
|
|
||||||
<div
|
|
||||||
v-if="devExtensions.length > 0"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<h2 class="text-lg font-semibold">{{ t('list.title') }}</h2>
|
|
||||||
|
|
||||||
<UCard
|
|
||||||
v-for="ext in devExtensions"
|
|
||||||
:key="ext.id"
|
|
||||||
class="p-4 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<h3 class="font-medium">{{ ext.name }}</h3>
|
|
||||||
<UBadge color="info">DEV</UBadge>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm opacity-70">v{{ ext.version }}</p>
|
|
||||||
<p class="text-xs opacity-50">{{ ext.publicKey.slice(0, 16) }}...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<UiButton
|
|
||||||
:label="t('list.reload')"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="reloadDevExtensionAsync(ext)"
|
|
||||||
/>
|
|
||||||
<UiButton
|
|
||||||
:label="t('list.remove')"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
color="error"
|
|
||||||
@click="removeDevExtensionAsync(ext)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="text-center py-8 opacity-50"
|
|
||||||
>
|
|
||||||
{{ t('list.empty') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
name: 'settings-developer',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const { add } = useToast()
|
|
||||||
const { loadExtensionsAsync } = useExtensionsStore()
|
|
||||||
|
|
||||||
|
|
||||||
// State
|
|
||||||
const extensionPath = ref('')
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const devExtensions = ref<
|
|
||||||
Array<{
|
|
||||||
id: string
|
|
||||||
publicKey: string
|
|
||||||
name: string
|
|
||||||
version: string
|
|
||||||
enabled: boolean
|
|
||||||
}>
|
|
||||||
>([])
|
|
||||||
|
|
||||||
// Load dev extensions on mount
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadDevExtensionListAsync()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Browse for extension directory
|
|
||||||
const browseExtensionPathAsync = async () => {
|
|
||||||
try {
|
|
||||||
const selected = await open({
|
|
||||||
directory: true,
|
|
||||||
multiple: false,
|
|
||||||
title: t('add.browseTitle'),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (selected && typeof selected === 'string') {
|
|
||||||
extensionPath.value = selected
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to browse directory:', error)
|
|
||||||
add({
|
|
||||||
description: t('add.errors.browseFailed'),
|
|
||||||
color: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load a dev extension
|
|
||||||
const loadDevExtensionAsync = async () => {
|
|
||||||
if (!extensionPath.value) return
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const extensionId = await invoke<string>('load_dev_extension', {
|
|
||||||
extensionPath: extensionPath.value,
|
|
||||||
})
|
|
||||||
|
|
||||||
add({
|
|
||||||
description: t('add.success'),
|
|
||||||
color: 'success',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reload list
|
|
||||||
await loadDevExtensionListAsync()
|
|
||||||
|
|
||||||
// Reload all extensions in the main extension store so they appear in the launcher
|
|
||||||
await loadExtensionsAsync()
|
|
||||||
|
|
||||||
// Clear input
|
|
||||||
extensionPath.value = ''
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to load dev extension:', error)
|
|
||||||
add({
|
|
||||||
description: error || t('add.errors.loadFailed'),
|
|
||||||
color: 'error',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all dev extensions (for the list on this page)
|
|
||||||
const loadDevExtensionListAsync = async () => {
|
|
||||||
try {
|
|
||||||
const extensions = await invoke<Array<any>>('get_all_dev_extensions')
|
|
||||||
devExtensions.value = extensions
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load dev extensions:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload a dev extension (removes and re-adds)
|
|
||||||
const reloadDevExtensionAsync = async (ext: any) => {
|
|
||||||
try {
|
|
||||||
// Get the extension path from somewhere (we need to store this)
|
|
||||||
// For now, just show a message
|
|
||||||
add({
|
|
||||||
description: t('list.reloadInfo'),
|
|
||||||
color: 'info',
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to reload dev extension:', error)
|
|
||||||
add({
|
|
||||||
description: error || t('list.errors.reloadFailed'),
|
|
||||||
color: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a dev extension
|
|
||||||
const removeDevExtensionAsync = async (ext: any) => {
|
|
||||||
try {
|
|
||||||
await invoke('remove_dev_extension', {
|
|
||||||
publicKey: ext.publicKey,
|
|
||||||
name: ext.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
add({
|
|
||||||
description: t('list.removeSuccess'),
|
|
||||||
color: 'success',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reload list
|
|
||||||
await loadDevExtensionListAsync()
|
|
||||||
|
|
||||||
// Reload all extensions store
|
|
||||||
await loadExtensionsAsync()
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to remove dev extension:', error)
|
|
||||||
add({
|
|
||||||
description: error || t('list.errors.removeFailed'),
|
|
||||||
color: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i18n lang="yaml">
|
|
||||||
de:
|
|
||||||
title: Entwicklereinstellungen
|
|
||||||
description: Lade Extensions im Entwicklungsmodus für schnelleres Testen mit Hot-Reload.
|
|
||||||
add:
|
|
||||||
title: Dev-Extension hinzufügen
|
|
||||||
extensionPath: Extension-Pfad
|
|
||||||
extensionPathPlaceholder: /pfad/zu/deiner/extension
|
|
||||||
extensionPathHint: Pfad zum Extension-Projekt (enthält haextension/ und haextension.json)
|
|
||||||
browse: Durchsuchen
|
|
||||||
browseTitle: Extension-Verzeichnis auswählen
|
|
||||||
loadExtension: Extension laden
|
|
||||||
success: Dev-Extension erfolgreich geladen
|
|
||||||
errors:
|
|
||||||
browseFailed: Verzeichnis konnte nicht ausgewählt werden
|
|
||||||
loadFailed: Extension konnte nicht geladen werden
|
|
||||||
list:
|
|
||||||
title: Geladene Dev-Extensions
|
|
||||||
empty: Keine Dev-Extensions geladen
|
|
||||||
reload: Neu laden
|
|
||||||
remove: Entfernen
|
|
||||||
reloadInfo: Extension wird beim nächsten Laden automatisch aktualisiert
|
|
||||||
removeSuccess: Dev-Extension erfolgreich entfernt
|
|
||||||
errors:
|
|
||||||
reloadFailed: Extension konnte nicht neu geladen werden
|
|
||||||
removeFailed: Extension konnte nicht entfernt werden
|
|
||||||
|
|
||||||
en:
|
|
||||||
title: Developer Settings
|
|
||||||
description: Load extensions in development mode for faster testing with hot-reload.
|
|
||||||
add:
|
|
||||||
title: Add Dev Extension
|
|
||||||
extensionPath: Extension Path
|
|
||||||
extensionPathPlaceholder: /path/to/your/extension
|
|
||||||
extensionPathHint: Path to your extension project (contains haextension/ and haextension.json)
|
|
||||||
browse: Browse
|
|
||||||
browseTitle: Select Extension Directory
|
|
||||||
loadExtension: Load Extension
|
|
||||||
success: Dev extension loaded successfully
|
|
||||||
errors:
|
|
||||||
browseFailed: Failed to select directory
|
|
||||||
loadFailed: Failed to load extension
|
|
||||||
list:
|
|
||||||
title: Loaded Dev Extensions
|
|
||||||
empty: No dev extensions loaded
|
|
||||||
reload: Reload
|
|
||||||
remove: Remove
|
|
||||||
reloadInfo: Extension will be automatically updated on next load
|
|
||||||
removeSuccess: Dev extension removed successfully
|
|
||||||
errors:
|
|
||||||
reloadFailed: Failed to reload extension
|
|
||||||
removeFailed: Failed to remove extension
|
|
||||||
</i18n>
|
|
||||||
@ -7,7 +7,7 @@ import type {
|
|||||||
import de from './de.json'
|
import de from './de.json'
|
||||||
import en from './en.json'
|
import en from './en.json'
|
||||||
|
|
||||||
export type DesktopItemType = 'extension' | 'file' | 'folder'
|
export type DesktopItemType = 'extension' | 'file' | 'folder' | 'system'
|
||||||
|
|
||||||
export interface IDesktopItem extends SelectHaexDesktopItems {
|
export interface IDesktopItem extends SelectHaexDesktopItems {
|
||||||
label?: string
|
label?: string
|
||||||
@ -57,18 +57,20 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
|||||||
referenceId: string,
|
referenceId: string,
|
||||||
positionX: number = 0,
|
positionX: number = 0,
|
||||||
positionY: number = 0,
|
positionY: number = 0,
|
||||||
|
workspaceId?: string,
|
||||||
) => {
|
) => {
|
||||||
if (!currentVault.value?.drizzle) {
|
if (!currentVault.value?.drizzle) {
|
||||||
throw new Error('Kein Vault geöffnet')
|
throw new Error('Kein Vault geöffnet')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentWorkspace.value) {
|
const targetWorkspaceId = workspaceId || currentWorkspace.value?.id
|
||||||
|
if (!targetWorkspaceId) {
|
||||||
throw new Error('Kein Workspace aktiv')
|
throw new Error('Kein Workspace aktiv')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newItem: InsertHaexDesktopItems = {
|
const newItem: InsertHaexDesktopItems = {
|
||||||
workspaceId: currentWorkspace.value.id,
|
workspaceId: targetWorkspaceId,
|
||||||
itemType: itemType,
|
itemType: itemType,
|
||||||
referenceId: referenceId,
|
referenceId: referenceId,
|
||||||
positionX: positionX,
|
positionX: positionX,
|
||||||
@ -85,7 +87,19 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
|||||||
return result[0]
|
return result[0]
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Hinzufügen des Desktop-Items:', error)
|
console.error('Fehler beim Hinzufügen des Desktop-Items:', {
|
||||||
|
error,
|
||||||
|
itemType,
|
||||||
|
referenceId,
|
||||||
|
workspaceId: targetWorkspaceId,
|
||||||
|
position: { x: positionX, y: positionY }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Log full error details
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
console.error('Full error object:', JSON.stringify(error, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,8 +168,23 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
|||||||
referenceId: string,
|
referenceId: string,
|
||||||
sourcePosition?: { x: number; y: number; width: number; height: number },
|
sourcePosition?: { x: number; y: number; width: number; height: number },
|
||||||
) => {
|
) => {
|
||||||
if (itemType === 'extension') {
|
const windowManager = useWindowManagerStore()
|
||||||
const windowManager = useWindowManagerStore()
|
|
||||||
|
if (itemType === 'system') {
|
||||||
|
const systemWindow = windowManager.getAllSystemWindows().find(
|
||||||
|
(win) => win.id === referenceId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (systemWindow) {
|
||||||
|
windowManager.openWindowAsync({
|
||||||
|
sourceId: systemWindow.id,
|
||||||
|
type: 'system',
|
||||||
|
icon: systemWindow.icon,
|
||||||
|
title: systemWindow.name,
|
||||||
|
sourcePosition,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (itemType === 'extension') {
|
||||||
const extensionsStore = useExtensionsStore()
|
const extensionsStore = useExtensionsStore()
|
||||||
|
|
||||||
const extension = extensionsStore.availableExtensions.find(
|
const extension = extensionsStore.availableExtensions.find(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { eq } from 'drizzle-orm'
|
import { asc, eq } from 'drizzle-orm'
|
||||||
import {
|
import {
|
||||||
haexWorkspaces,
|
haexWorkspaces,
|
||||||
type SelectHaexWorkspaces,
|
type SelectHaexWorkspaces,
|
||||||
@ -32,18 +32,18 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* const items = await currentVault.value.drizzle
|
const items = await currentVault.value.drizzle
|
||||||
.select()
|
.select()
|
||||||
.from(haexWorkspaces)
|
.from(haexWorkspaces)
|
||||||
.orderBy(asc(haexWorkspaces.position))
|
.orderBy(asc(haexWorkspaces.position))
|
||||||
|
|
||||||
console.log('loadWorkspacesAsync', items)
|
console.log('loadWorkspacesAsync', items)
|
||||||
workspaces.value = items */
|
workspaces.value = items
|
||||||
|
|
||||||
// Create default workspace if none exist
|
// Create default workspace if none exist
|
||||||
/* if (items.length === 0) { */
|
if (items.length === 0) {
|
||||||
await addWorkspaceAsync('Workspace 1')
|
await addWorkspaceAsync('Workspace 1')
|
||||||
/* } */
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der Workspaces:', error)
|
console.error('Fehler beim Laden der Workspaces:', error)
|
||||||
throw error
|
throw error
|
||||||
@ -61,15 +61,12 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const newIndex = workspaces.value.length + 1
|
const newIndex = workspaces.value.length + 1
|
||||||
const newWorkspace: SelectHaexWorkspaces = {
|
const newWorkspace = {
|
||||||
id: crypto.randomUUID(),
|
|
||||||
name: name || `Workspace ${newIndex}`,
|
name: name || `Workspace ${newIndex}`,
|
||||||
position: workspaces.value.length,
|
position: workspaces.value.length,
|
||||||
haexTimestamp: '',
|
|
||||||
}
|
}
|
||||||
workspaces.value.push(newWorkspace)
|
|
||||||
currentWorkspaceIndex.value = workspaces.value.length - 1
|
const result = await currentVault.value.drizzle
|
||||||
/* const result = await currentVault.value.drizzle
|
|
||||||
.insert(haexWorkspaces)
|
.insert(haexWorkspaces)
|
||||||
.values(newWorkspace)
|
.values(newWorkspace)
|
||||||
.returning()
|
.returning()
|
||||||
@ -78,7 +75,7 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
workspaces.value.push(result[0])
|
workspaces.value.push(result[0])
|
||||||
currentWorkspaceIndex.value = workspaces.value.length - 1
|
currentWorkspaceIndex.value = workspaces.value.length - 1
|
||||||
return result[0]
|
return result[0]
|
||||||
} */
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Hinzufügen des Workspace:', error)
|
console.error('Fehler beim Hinzufügen des Workspace:', error)
|
||||||
throw error
|
throw error
|
||||||
@ -106,27 +103,27 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
|
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
|
|
||||||
workspaces.value.splice(index, 1)
|
|
||||||
workspaces.value.forEach((workspace, index) => (workspace.position = index))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* await currentVault.value.drizzle.transaction(async (tx) => {
|
await currentVault.value.drizzle.transaction(async (tx) => {
|
||||||
|
// Delete workspace
|
||||||
await tx
|
await tx
|
||||||
.delete(haexWorkspaces)
|
.delete(haexWorkspaces)
|
||||||
.where(eq(haexWorkspaces.id, workspaceId))
|
.where(eq(haexWorkspaces.id, workspaceId))
|
||||||
|
|
||||||
|
// Update local state
|
||||||
workspaces.value.splice(index, 1)
|
workspaces.value.splice(index, 1)
|
||||||
workspaces.value.forEach(
|
workspaces.value.forEach((workspace, idx) => {
|
||||||
(workspace, index) => (workspace.position = index),
|
workspace.position = idx
|
||||||
)
|
})
|
||||||
|
|
||||||
|
// Update positions in database
|
||||||
for (const workspace of workspaces.value) {
|
for (const workspace of workspaces.value) {
|
||||||
await tx
|
await tx
|
||||||
.update(haexWorkspaces)
|
.update(haexWorkspaces)
|
||||||
.set({ position: index })
|
.set({ position: workspace.position })
|
||||||
.where(eq(haexWorkspaces.position, workspace.position))
|
.where(eq(haexWorkspaces.id, workspace.id))
|
||||||
}
|
}
|
||||||
}) */
|
})
|
||||||
|
|
||||||
// Adjust current index if needed
|
// Adjust current index if needed
|
||||||
if (currentWorkspaceIndex.value >= workspaces.value.length) {
|
if (currentWorkspaceIndex.value >= workspaces.value.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user