mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
fix window on workspace rendering
This commit is contained in:
@ -156,11 +156,11 @@ export const haexDesktopItems = sqliteTable(
|
||||
.notNull()
|
||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||
enum: ['extension', 'file', 'folder'],
|
||||
enum: ['system', 'extension', 'file', 'folder'],
|
||||
}).notNull(),
|
||||
referenceId: text(
|
||||
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)
|
||||
.notNull()
|
||||
.default(0),
|
||||
|
||||
@ -88,30 +88,64 @@ impl ExtensionManager {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// Check if manifest.json is directly in temp or in a subdirectory
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let actual_dir = if manifest_path.exists() {
|
||||
temp.clone()
|
||||
} else {
|
||||
// manifest.json is in a subdirectory - find it
|
||||
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 entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() && path.join("manifest.json").exists() {
|
||||
found_dir = Some(path);
|
||||
break;
|
||||
}
|
||||
// Read haextension_dir from config if it exists, otherwise use default
|
||||
let config_path = temp.join("haextension.config.json");
|
||||
let haextension_dir = if config_path.exists() {
|
||||
let config_content = std::fs::read_to_string(&config_path)
|
||||
.map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
let config: serde_json::Value = serde_json::from_str(&config_content)
|
||||
.map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Invalid haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
let dir = config
|
||||
.get("dev")
|
||||
.and_then(|dev| dev.get("haextension_dir"))
|
||||
.and_then(|dir| dir.as_str())
|
||||
.unwrap_or("haextension")
|
||||
.to_string();
|
||||
|
||||
// Security: Validate that haextension_dir doesn't contain ".." 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 {
|
||||
reason: "manifest.json not found in extension archive".to_string(),
|
||||
})?
|
||||
dir
|
||||
} 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 =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
@ -119,7 +153,7 @@ impl ExtensionManager {
|
||||
|
||||
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 {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
|
||||
@ -4,28 +4,13 @@ use std::{
|
||||
};
|
||||
|
||||
// src-tauri/src/extension/crypto.rs
|
||||
use crate::extension::error::ExtensionError;
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub struct 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
|
||||
pub fn verify_signature(
|
||||
public_key_hex: &str,
|
||||
@ -50,26 +35,48 @@ impl ExtensionCrypto {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let mut all_files = Vec::new();
|
||||
Self::collect_files_recursively(dir, &mut all_files)
|
||||
.map_err(|e| format!("Failed to collect files: {}", e))?;
|
||||
all_files.sort();
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// 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 manifest_path = dir.join("manifest.json");
|
||||
|
||||
// 2. Inhalte der sortierten Dateien hashen
|
||||
for file_path in all_files {
|
||||
// 4. Inhalte der sortierten Dateien hashen
|
||||
for (_relative, file_path) in relative_files {
|
||||
if file_path == manifest_path {
|
||||
// FÜR DIE MANIFEST.JSON:
|
||||
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
|
||||
let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
|
||||
.map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
|
||||
let mut manifest: serde_json::Value =
|
||||
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
|
||||
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)
|
||||
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);
|
||||
hasher.update(canonical_manifest_content.as_bytes());
|
||||
} else {
|
||||
// FÜR ALLE ANDEREN DATEIEN:
|
||||
let content = fs::read(&file_path)
|
||||
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
|
||||
let content =
|
||||
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
hasher.update(&content);
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,6 +223,16 @@ pub fn is_extension_installed(
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct HaextensionConfig {
|
||||
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)]
|
||||
@ -231,6 +241,8 @@ struct DevConfig {
|
||||
port: u16,
|
||||
#[serde(default = "default_host")]
|
||||
host: String,
|
||||
#[serde(default = "default_haextension_dir")]
|
||||
haextension_dir: String,
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
@ -241,6 +253,10 @@ fn default_host() -> 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
|
||||
async fn check_dev_server_health(url: &str) -> bool {
|
||||
use tauri_plugin_http::reqwest;
|
||||
@ -276,29 +292,30 @@ pub async fn load_dev_extension(
|
||||
|
||||
let extension_path_buf = PathBuf::from(&extension_path);
|
||||
|
||||
// 1. Read haextension.json to get dev server config
|
||||
let config_path = extension_path_buf.join("haextension.json");
|
||||
let (host, port) = if config_path.exists() {
|
||||
// 1. Read haextension.config.json to get dev server config and haextension directory
|
||||
let config_path = extension_path_buf.join("haextension.config.json");
|
||||
let (host, port, haextension_dir) = if config_path.exists() {
|
||||
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
|
||||
ExtensionError::ValidationError {
|
||||
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| {
|
||||
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 {
|
||||
// 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);
|
||||
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
||||
eprintln!("📁 Haextension directory: {}", haextension_dir);
|
||||
|
||||
// 1.5. Check if dev server is running
|
||||
if !check_dev_server_health(&dev_server_url).await {
|
||||
@ -311,8 +328,8 @@ pub async fn load_dev_extension(
|
||||
}
|
||||
eprintln!("✅ Dev server is reachable");
|
||||
|
||||
// 2. Build path to manifest: <extension_path>/haextension/manifest.json
|
||||
let manifest_path = extension_path_buf.join("haextension").join("manifest.json");
|
||||
// 2. Build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||
let manifest_path = extension_path_buf.join(&haextension_dir).join("manifest.json");
|
||||
|
||||
// Check if manifest exists
|
||||
if !manifest_path.exists() {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
:alt="label"
|
||||
class="w-14 h-14 object-contain transition-transform duration-200"
|
||||
:class="{ 'scale-110': isSelected }"
|
||||
>
|
||||
/>
|
||||
<UIcon
|
||||
v-else
|
||||
name="i-heroicons-puzzle-piece-solid"
|
||||
@ -69,7 +69,7 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
itemType: 'extension' | 'file' | 'folder'
|
||||
itemType: DesktopItemType
|
||||
referenceId: string
|
||||
initialX: number
|
||||
initialY: number
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
class="w-full h-full relative isolate"
|
||||
@click.self.stop="handleDesktopClick"
|
||||
@mousedown.left.self="handleAreaSelectStart"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@drop.prevent="handleDrop($event, workspace.id)"
|
||||
>
|
||||
<!-- Grid Pattern Background -->
|
||||
<div
|
||||
@ -81,128 +83,113 @@
|
||||
v-for="window in getWorkspaceWindows(workspace.id)"
|
||||
:key="window.id"
|
||||
>
|
||||
<!-- Desktop container for when overview is closed -->
|
||||
<div
|
||||
:id="`desktop-container-${window.id}`"
|
||||
class="absolute"
|
||||
/>
|
||||
|
||||
<!-- Window with dynamic teleport -->
|
||||
<!-- Overview Mode: Teleport to window preview -->
|
||||
<Teleport
|
||||
:to="
|
||||
windowManager.showWindowOverview &&
|
||||
overviewWindowState.has(window.id)
|
||||
? `#window-preview-${window.id}`
|
||||
: `#desktop-container-${window.id}`
|
||||
"
|
||||
v-if="windowManager.showWindowOverview && overviewWindowState.has(window.id)"
|
||||
:to="`#window-preview-${window.id}`"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
windowManager.showWindowOverview &&
|
||||
overviewWindowState.has(window.id)
|
||||
"
|
||||
<div
|
||||
class="absolute origin-top-left"
|
||||
:style="{
|
||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute origin-top-left"
|
||||
:style="{
|
||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||
}"
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
: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"
|
||||
>
|
||||
<HaexWindow
|
||||
v-show="
|
||||
windowManager.showWindowOverview || !window.isMinimized
|
||||
"
|
||||
: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'"
|
||||
/>
|
||||
<!-- 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>
|
||||
</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>
|
||||
<!-- Extension Window: Render iFrame -->
|
||||
<HaexDesktopExtensionFrame
|
||||
v-else
|
||||
:extension-id="window.sourceId"
|
||||
:window-id="window.id"
|
||||
/>
|
||||
</HaexWindow>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
@ -342,6 +329,18 @@ const getWorkspaceIcons = (workspaceId: string) => {
|
||||
return desktopItems.value
|
||||
.filter((item) => item.workspaceId === workspaceId)
|
||||
.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') {
|
||||
const extension = availableExtensions.value.find(
|
||||
(ext) => ext.id === item.referenceId,
|
||||
@ -416,6 +415,49 @@ const handleDragEnd = async () => {
|
||||
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 = () => {
|
||||
// Only clear selection if it was a simple click, not an area selection
|
||||
// Check if we just finished an area selection (box size > threshold)
|
||||
|
||||
@ -11,22 +11,29 @@
|
||||
<template #content>
|
||||
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
||||
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
||||
<UiButton
|
||||
<UContextMenu
|
||||
v-for="item in launcherItems"
|
||||
:key="item.id"
|
||||
square
|
||||
size="xl"
|
||||
variant="ghost"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
:icon="item.icon"
|
||||
:label="item.name"
|
||||
:tooltip="item.name"
|
||||
@click="openItem(item)"
|
||||
/>
|
||||
:items="getContextMenuItems(item)"
|
||||
>
|
||||
<UiButton
|
||||
square
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
:ui="{
|
||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
|
||||
leadingIcon: 'size-10',
|
||||
label: 'w-full',
|
||||
}"
|
||||
: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) -->
|
||||
<UiButton
|
||||
@ -119,14 +126,77 @@ const openItem = async (item: LauncherItem) => {
|
||||
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>
|
||||
|
||||
<i18n lang="yaml">
|
||||
de:
|
||||
disabled: Deaktiviert
|
||||
marketplace: Marketplace
|
||||
contextMenu:
|
||||
open: Öffnen
|
||||
uninstall: Deinstallieren
|
||||
|
||||
en:
|
||||
disabled: Disabled
|
||||
marketplace: Marketplace
|
||||
contextMenu:
|
||||
open: Open
|
||||
uninstall: Uninstall
|
||||
</i18n>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<!-- Window Thumbnails Flex Layout -->
|
||||
<div
|
||||
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
|
||||
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 en from './en.json'
|
||||
|
||||
export type DesktopItemType = 'extension' | 'file' | 'folder'
|
||||
export type DesktopItemType = 'extension' | 'file' | 'folder' | 'system'
|
||||
|
||||
export interface IDesktopItem extends SelectHaexDesktopItems {
|
||||
label?: string
|
||||
@ -57,18 +57,20 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
referenceId: string,
|
||||
positionX: number = 0,
|
||||
positionY: number = 0,
|
||||
workspaceId?: string,
|
||||
) => {
|
||||
if (!currentVault.value?.drizzle) {
|
||||
throw new Error('Kein Vault geöffnet')
|
||||
}
|
||||
|
||||
if (!currentWorkspace.value) {
|
||||
const targetWorkspaceId = workspaceId || currentWorkspace.value?.id
|
||||
if (!targetWorkspaceId) {
|
||||
throw new Error('Kein Workspace aktiv')
|
||||
}
|
||||
|
||||
try {
|
||||
const newItem: InsertHaexDesktopItems = {
|
||||
workspaceId: currentWorkspace.value.id,
|
||||
workspaceId: targetWorkspaceId,
|
||||
itemType: itemType,
|
||||
referenceId: referenceId,
|
||||
positionX: positionX,
|
||||
@ -85,7 +87,19 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
return result[0]
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@ -154,8 +168,23 @@ export const useDesktopStore = defineStore('desktopStore', () => {
|
||||
referenceId: string,
|
||||
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 extension = extensionsStore.availableExtensions.find(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { asc, eq } from 'drizzle-orm'
|
||||
import {
|
||||
haexWorkspaces,
|
||||
type SelectHaexWorkspaces,
|
||||
@ -32,18 +32,18 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
}
|
||||
|
||||
try {
|
||||
/* const items = await currentVault.value.drizzle
|
||||
const items = await currentVault.value.drizzle
|
||||
.select()
|
||||
.from(haexWorkspaces)
|
||||
.orderBy(asc(haexWorkspaces.position))
|
||||
|
||||
console.log('loadWorkspacesAsync', items)
|
||||
workspaces.value = items */
|
||||
workspaces.value = items
|
||||
|
||||
// Create default workspace if none exist
|
||||
/* if (items.length === 0) { */
|
||||
await addWorkspaceAsync('Workspace 1')
|
||||
/* } */
|
||||
if (items.length === 0) {
|
||||
await addWorkspaceAsync('Workspace 1')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Workspaces:', error)
|
||||
throw error
|
||||
@ -61,15 +61,12 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
|
||||
try {
|
||||
const newIndex = workspaces.value.length + 1
|
||||
const newWorkspace: SelectHaexWorkspaces = {
|
||||
id: crypto.randomUUID(),
|
||||
const newWorkspace = {
|
||||
name: name || `Workspace ${newIndex}`,
|
||||
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)
|
||||
.values(newWorkspace)
|
||||
.returning()
|
||||
@ -78,7 +75,7 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
workspaces.value.push(result[0])
|
||||
currentWorkspaceIndex.value = workspaces.value.length - 1
|
||||
return result[0]
|
||||
} */
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Workspace:', error)
|
||||
throw error
|
||||
@ -106,27 +103,27 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
||||
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
|
||||
if (index === -1) return
|
||||
|
||||
workspaces.value.splice(index, 1)
|
||||
workspaces.value.forEach((workspace, index) => (workspace.position = index))
|
||||
|
||||
try {
|
||||
/* await currentVault.value.drizzle.transaction(async (tx) => {
|
||||
await currentVault.value.drizzle.transaction(async (tx) => {
|
||||
// Delete workspace
|
||||
await tx
|
||||
.delete(haexWorkspaces)
|
||||
.where(eq(haexWorkspaces.id, workspaceId))
|
||||
|
||||
// Update local state
|
||||
workspaces.value.splice(index, 1)
|
||||
workspaces.value.forEach(
|
||||
(workspace, index) => (workspace.position = index),
|
||||
)
|
||||
workspaces.value.forEach((workspace, idx) => {
|
||||
workspace.position = idx
|
||||
})
|
||||
|
||||
// Update positions in database
|
||||
for (const workspace of workspaces.value) {
|
||||
await tx
|
||||
.update(haexWorkspaces)
|
||||
.set({ position: index })
|
||||
.where(eq(haexWorkspaces.position, workspace.position))
|
||||
.set({ position: workspace.position })
|
||||
.where(eq(haexWorkspaces.id, workspace.id))
|
||||
}
|
||||
}) */
|
||||
})
|
||||
|
||||
// Adjust current index if needed
|
||||
if (currentWorkspaceIndex.value >= workspaces.value.length) {
|
||||
|
||||
Reference in New Issue
Block a user